Repository: microsoft/teams-ai Branch: main Commit: f393932802d7 Files: 318 Total size: 858.9 KB Directory structure: gitextract_6wy_q17s/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── deploy-teams-docs.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── package.json ├── packages/ │ └── README.md ├── prettier.config.js ├── teams.md/ │ ├── .gitattributes │ ├── .gitignore │ ├── LANGUAGE-INCLUDE.md │ ├── README.md │ ├── docs/ │ │ └── main/ │ │ ├── developer-tools/ │ │ │ ├── README.md │ │ │ ├── _category_.json │ │ │ ├── cli.md │ │ │ └── devtools/ │ │ │ ├── README.md │ │ │ ├── cards.md │ │ │ ├── chat.md │ │ │ └── inspect.md │ │ ├── privacy.md │ │ ├── teams/ │ │ │ ├── README.md │ │ │ ├── _category_.json │ │ │ ├── app-authentication/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── client-secret.md │ │ │ │ ├── federated-identity-credentials.md │ │ │ │ ├── troubleshooting.md │ │ │ │ └── user-managed-identity.md │ │ │ ├── configuration/ │ │ │ │ ├── README.md │ │ │ │ ├── _category_.json │ │ │ │ ├── agents-toolkit.md │ │ │ │ └── manual-configuration.mdx │ │ │ ├── core-concepts.md │ │ │ ├── enabling-in-copilot.md │ │ │ ├── manifest.md │ │ │ └── user-authentication/ │ │ │ ├── README.md │ │ │ ├── _category_.json │ │ │ ├── sso-setup.mdx │ │ │ └── troubleshooting-sso.mdx │ │ ├── welcome.mdx │ │ └── why.md │ ├── docusaurus.config.ts │ ├── i18n/ │ │ └── en/ │ │ ├── code.json │ │ ├── docusaurus-plugin-content-docs/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-docs-csharp/ │ │ │ └── current.json │ │ ├── docusaurus-plugin-content-docs-typescript/ │ │ │ └── current.json │ │ └── docusaurus-theme-classic/ │ │ ├── footer.json │ │ └── navbar.json │ ├── package.json │ ├── scripts/ │ │ ├── generate-language-docs.ts │ │ ├── generate-llms-txt.ts │ │ └── lib/ │ │ ├── content-processor.ts │ │ ├── file-collector.ts │ │ └── frontmatter-parser.ts │ ├── sidebars.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── FileCodeBlock.tsx │ │ │ ├── LangLink.tsx │ │ │ ├── Language.tsx │ │ │ ├── LanguageBanner.tsx │ │ │ ├── LanguageDropdown.tsx │ │ │ └── include/ │ │ │ ├── essentials/ │ │ │ │ ├── api/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── app-authentication/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── csharp.incl.md │ │ │ │ ├── graph/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── on-activity/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── on-event/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── python.incl.md │ │ │ │ ├── sending-messages/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── proactive-messaging/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ └── typescript.incl.md │ │ │ ├── getting-started/ │ │ │ │ ├── _LLMs/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── code-basics/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── csharp.incl.md │ │ │ │ ├── python.incl.md │ │ │ │ ├── quickstart/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── running-in-teams/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ └── typescript.incl.md │ │ │ ├── in-depth-guides/ │ │ │ │ ├── adaptive-cards/ │ │ │ │ │ ├── building-adaptive-cards/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── executing-actions/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── ai/ │ │ │ │ │ ├── a2a/ │ │ │ │ │ │ ├── a2a-client/ │ │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ │ ├── a2a-server/ │ │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── best-practices/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── chat/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── function-calling/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── keeping-state/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── mcp/ │ │ │ │ │ │ ├── mcp-client/ │ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ │ └── mcp-server/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ ├── setup-and-prereqs/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── csharp.incl.md │ │ │ │ ├── dialogs/ │ │ │ │ │ ├── creating-dialogs/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── handling-dialog-submissions/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── handling-multi-step-forms/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── feedback/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── meeting-events/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── message-extensions/ │ │ │ │ │ ├── action-commands/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── link-unfurling/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ ├── search-commands/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── settings/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── observability/ │ │ │ │ │ ├── logging/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── middleware/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── python.incl.md │ │ │ │ ├── server/ │ │ │ │ │ ├── http-server/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ ├── python.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── static-pages/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── tabs/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── functions/ │ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ │ └── typescript.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── typescript.incl.md │ │ │ │ └── user-authentication/ │ │ │ │ ├── csharp.incl.md │ │ │ │ ├── python.incl.md │ │ │ │ └── typescript.incl.md │ │ │ └── migrations/ │ │ │ ├── botbuilder/ │ │ │ │ ├── integration/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── proactive-activities/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── sending-activities/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ ├── the-api-client/ │ │ │ │ │ ├── csharp.incl.md │ │ │ │ │ ├── python.incl.md │ │ │ │ │ └── typescript.incl.md │ │ │ │ └── user-authentication/ │ │ │ │ ├── csharp.incl.md │ │ │ │ ├── python.incl.md │ │ │ │ └── typescript.incl.md │ │ │ ├── slack-bolt/ │ │ │ │ └── typescript.incl.md │ │ │ ├── v1/ │ │ │ │ ├── python.incl.md │ │ │ │ └── typescript.incl.md │ │ │ └── v2-previews/ │ │ │ └── typescript.incl.md │ │ ├── constants/ │ │ │ └── languages.ts │ │ ├── css/ │ │ │ ├── code-blocks.css │ │ │ └── custom.css │ │ ├── hooks/ │ │ │ └── useLanguagePreference.tsx │ │ ├── pages/ │ │ │ ├── csharp.tsx │ │ │ ├── index.module.css │ │ │ ├── index.tsx │ │ │ ├── python.tsx │ │ │ ├── templates/ │ │ │ │ ├── essentials/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── app-authentication.mdx │ │ │ │ │ ├── app-basics.mdx │ │ │ │ │ ├── graph.mdx │ │ │ │ │ ├── on-activity/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ └── activity-ref.mdx │ │ │ │ │ ├── on-event.mdx │ │ │ │ │ └── sending-messages/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ ├── _category_.json │ │ │ │ │ └── proactive-messaging.mdx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ ├── _LLMs.mdx │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── code-basics.mdx │ │ │ │ │ ├── quickstart.mdx │ │ │ │ │ └── running-in-teams/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ └── _category_.json │ │ │ │ ├── in-depth-guides/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── adaptive-cards/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── building-adaptive-cards.mdx │ │ │ │ │ │ └── executing-actions.mdx │ │ │ │ │ ├── ai/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── a2a/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ │ ├── a2a-client.mdx │ │ │ │ │ │ │ └── a2a-server.mdx │ │ │ │ │ │ ├── best-practices.mdx │ │ │ │ │ │ ├── chat.mdx │ │ │ │ │ │ ├── function-calling.mdx │ │ │ │ │ │ ├── keeping-state.mdx │ │ │ │ │ │ ├── mcp/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ │ ├── mcp-client.mdx │ │ │ │ │ │ │ └── mcp-server.mdx │ │ │ │ │ │ └── setup-and-prereqs.mdx │ │ │ │ │ ├── dialogs/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── creating-dialogs.mdx │ │ │ │ │ │ ├── handling-dialog-submissions.mdx │ │ │ │ │ │ └── handling-multi-step-forms.mdx │ │ │ │ │ ├── feedback.mdx │ │ │ │ │ ├── meeting-events.mdx │ │ │ │ │ ├── message-extensions/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── action-commands.mdx │ │ │ │ │ │ ├── link-unfurling.mdx │ │ │ │ │ │ ├── search-commands.mdx │ │ │ │ │ │ └── settings.mdx │ │ │ │ │ ├── observability/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── logging.mdx │ │ │ │ │ │ └── middleware.mdx │ │ │ │ │ ├── server/ │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── http-server.mdx │ │ │ │ │ │ └── static-pages.mdx │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ ├── _category_.json │ │ │ │ │ │ ├── app-options.mdx │ │ │ │ │ │ ├── functions/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ └── function-calling.mdx │ │ │ │ │ │ ├── getting-started.mdx │ │ │ │ │ │ ├── graph.mdx │ │ │ │ │ │ └── using-the-app.mdx │ │ │ │ │ └── user-authentication.mdx │ │ │ │ └── migrations/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── botbuilder/ │ │ │ │ │ ├── README.mdx │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── integration.mdx │ │ │ │ │ ├── proactive-activities.mdx │ │ │ │ │ ├── sending-activities.mdx │ │ │ │ │ ├── the-api-client.mdx │ │ │ │ │ └── user-authentication.mdx │ │ │ │ ├── slack-bolt.mdx │ │ │ │ ├── v1.mdx │ │ │ │ └── v2-previews.mdx │ │ │ └── typescript.tsx │ │ ├── scripts/ │ │ │ └── scaffold.js │ │ ├── theme/ │ │ │ ├── DocSidebarItems/ │ │ │ │ └── index.tsx │ │ │ ├── Navbar/ │ │ │ │ └── Content/ │ │ │ │ └── index.tsx │ │ │ ├── PaginatorNavLink/ │ │ │ │ └── index.tsx │ │ │ └── Root.tsx │ │ └── utils/ │ │ ├── languageUtils.ts │ │ ├── normalizePath.ts │ │ ├── pageAvailability.ts │ │ └── readFileUtf8Normalized.ts │ ├── static/ │ │ ├── .nojekyll │ │ ├── llms_docs/ │ │ │ └── llms.txt │ │ ├── missing-pages.json │ │ └── scripts/ │ │ └── clarity.js │ └── tsconfig.json └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: - main paths: - 'teams.md/**' jobs: build-teams-md: name: Build teams.md runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - name: Install dependencies (teams.md) run: npm ci working-directory: teams.md - name: Clear build artifacts working-directory: teams.md run: npm run clear - name: Build (teams.md) env: NODE_ENV: production run: npm run build working-directory: teams.md ================================================ FILE: .github/workflows/deploy-teams-docs.yml ================================================ name: Deploy-Teams-Docs on: workflow_dispatch: push: branches: - main paths: - 'teams.md/**' - .github/workflows/deploy-teams-docs.yml jobs: deploy: environment: name: github-pages runs-on: ubuntu-latest permissions: contents: write # To push a branch pages: write id-token: write steps: - name: Checkout repo uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22.x' - name: Install dependencies working-directory: teams.md run: npm install - name: Clear build artifacts working-directory: teams.md run: npm run clear - name: Generate language docs working-directory: teams.md env: NODE_ENV: production run: npm run generate:docs - name: Generate llms.txt files working-directory: teams.md run: npm run generate:llms - name: Build static site working-directory: teams.md run: npm run build - name: Setup GitHub Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: teams.md/build - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Dependencies node_modules .pnp .pnp.js # Local env files .env .env.local .env.development.local .env.test.local .env.production.local # Testing coverage # Turbo .turbo # Vercel .vercel # Build Outputs .next/ out/ build dist # Generated documentation files teams.md/docs/main/typescript/ teams.md/docs/main/csharp/ teams.md/docs/main/python/ # Debug npm-debug.log* yarn-debug.log* yarn-error.log* # Misc .DS_Store *.pem .specstory ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .prettierignore ================================================ dist node_modules coverage ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["streetsidesoftware.code-spell-checker"] } ================================================ FILE: .vscode/settings.json ================================================ { "cSpell.words": [ "botframework", "devtunnels", "Entra", "mdbook" ] } ================================================ FILE: CONTRIBUTING.md ================================================ # Instructions for Contributing Code Teams SDK is a mono-repo that hosts GitHub submodules to other repos that contain code by language. The submodules in this repository uses the latest commit from those submodules. To make changes to Teams SDK Typescript, C#, Python, please use the following repos: - [Teams.ts](https://github.com/microsoft/teams.ts) - [Teams.net](https://github.com/microsoft/teams.net) - [Teams.py](https://github.com/microsoft/teams.py) Please note that as of August 2023, signed commits are required for all contributions to this project. For more information on how to set up, see the [GitHub Authentication verify commit signature](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) documentation. ## Contributing bug fixes and features 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.opensource.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., status check, 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. Microsoft Teams 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 Teams SDK 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. ## Contributing guide ### Documentation Please note that we place high importance on documentation, which we host as [Teams SDK github pages](https://microsoft.github.io/teams-sdk/). ### Testing changes Please use any of the agents in the `tests` directory of the repo you are writing in. These apps use the latest local changes and are intended to quickly set up and test feature work, bug fixes, etc. ================================================ FILE: LICENSE ================================================ Copyright (c) Microsoft Corporation. MIT License 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. ================================================ FILE: README.md ================================================ # Welcome to the Teams SDK ([Docs](https://microsoft.github.io/teams-sdk/)) Teams SDK represents a fundamental reimagining of how Teams apps and AI agents are built, while maintaining compatibility with existing botframework-based agents. This new version focuses on developer experience, simplified architecture, and enhanced AI capabilities. For a detailed explanation of the motivations and architectural decisions behind v2, please see our [WHY.md](https://microsoft.github.io/teams-sdk/why) document. ## We have a new name! We are very excited to announce that "Teams AI" is now officially "Teams SDK"! This change reflects our commitment to providing a comprehensive development framework for building all types of Teams applications, including AI-powered agents. ## Code repositories per SDK language The SDK code for each language are in individual repos: - [Typescript](https://github.com/microsoft/teams.ts) - [C#](https://github.com/microsoft/teams.net) - [Python](https://github.com/microsoft/teams.py) For language-specific bugs or issues, please use the Issues tab in the respective language repository. ## Agent Accelerator Templates You can find a set of open-source agent accelerator templates in the [Teams Agent Accelerators repository](https://github.com/microsoft/teams-agent-accelerator-templates). These templates provide a great starting point for building your own agents using the Teams SDK. ### Quick start The Teams SDK CLI makes it easy to bootstrap your first agent. First, install the CLI via NPM: ```sh npm install -g @microsoft/teams.cli ``` Next, use the CLI to create your agent: ```sh npx @microsoft/teams.cli new quote-agent --template echo ``` For more information, follow our quickstart guide: [C#](http://microsoft.github.io/teams-sdk/csharp/getting-started/quickstart), [Typescript](http://microsoft.github.io/teams-sdk/typescript/getting-started/quickstart), [Python](http://microsoft.github.io/teams-sdk/python/getting-started/quickstart) ### SDK Microsoft Teams has a robust developer ecosystem with a broad suite of capabilities, now unified via Teams SDK. Whether you are building AI-powered agents ([TS](https://microsoft.github.io/teams-sdk/typescript/in-depth-guides/ai/), [C#](https://microsoft.github.io/teams-sdk/csharp/in-depth-guides/ai/), [Python](https://microsoft.github.io/teams-sdk/python/in-depth-guides/ai/)), Message Extensions ([TS](https://microsoft.github.io/teams-sdk/typescript/in-depth-guides/message-extensions/), [C#](https://microsoft.github.io/teams-sdk/csharp/in-depth-guides/message-extensions/), [Python](https://microsoft.github.io/teams-sdk/python/in-depth-guides/message-extensions/)), embedded web applications, or Graph, Teams SDK has you covered. Here is a simple example, which responds to incoming messages with information retrieved from Graph. ```typescript import { App } from '@microsoft/teams.apps'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ plugins: [new DevtoolsPlugin()], }); // Listen for incoming messages app.on('message', async ({ userGraph, isSignedIn, send, signin }) => { if (!isSignedIn) { await signin(); // initiates Entra login flow return; } const me = await userGraph.call(endpoints.me.get); await send(`Hello, ${me.displayName} from Earth!`); }); // Start your application (async () => { await app.start(); })(); ``` For language-specific bugs or issues, please use the Issues tab in the respective language repository. ## Important: v1 to v2 Transition Notice **This repository has transitioned from v1 to v2 as the main branch.** - The `main` branch now contains v2 code, which was previously developed on the `v2-preview` branch. - The previous `main` branch (v1) has been moved to the [`release/v1`](https://github.com/microsoft/teams-sdk/tree/release/v1) branch. We will continue to provide critical bug fixes and security patches for v1 on this branch. ## Documentation For comprehensive documentation, API references, and examples, visit our [documentation site](https://microsoft.github.io/teams-sdk/). ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 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. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - Full paths of source file(s) related to the manifestation of the issue - The location of the affected source code (tag/branch/commit or direct URL) - Any special configuration required to reproduce the issue - Step-by-step instructions to reproduce the issue - Proof-of-concept or exploit code (if possible) - Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). ================================================ FILE: package.json ================================================ { "name": "teams-sdk", "private": true, "scripts": { "clean": "npx turbo clean", "build": "npx turbo build", "build:packages": "npx turbo build --filter=./packages/*", "dev": "npx turbo dev", "lint": "npx turbo lint", "lint:fix": "npx turbo lint:fix", "test": "npx turbo test --concurrency=100%", "fmt": "npx prettier --write \"**/*.{js,ts,tsx,md,json}\"", "docs:build": "npm -w teams-md run build", "docs:dev": "npm -w teams-md run start", "docs:serve": "npm -w teams-md run serve" }, "devDependencies": { "prettier": "^3.5.3", "turbo": "^2.5.0", "typescript": "5.8.2" }, "engines": { "node": ">=20" }, "packageManager": "npm@10.9.2", "workspaces": [ "packages/*", "teams.md" ], "tsconfig": { "exclude": [ "node_modules" ] } } ================================================ FILE: packages/README.md ================================================ # This is a blank file This is a blank folder currently. We will add packages that are common to all submodules here. ================================================ FILE: prettier.config.js ================================================ // prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs /** * @see https://prettier.io/docs/en/configuration.html * @type {import("prettier").Config} */ const config = { trailingComma: 'es5', semi: true, singleQuote: true, printWidth: 100, }; module.exports = config; ================================================ FILE: teams.md/.gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: teams.md/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader scripts/generated/ # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* static/llms_docs/ ================================================ FILE: teams.md/LANGUAGE-INCLUDE.md ================================================ # Language-Specific Documentation System ## Table of Contents - [Quick Start](#quick-start) - [Architecture](#architecture) - [Core Concepts](#core-concepts) - [Templates](#templates-srcpagestemplatesmdx) - [Fragments](#fragments-srccomponentsincludelangincmd) - [Commands](#commands) - [Language Filtering](#language-filtering) - [Page Types](#page-types) - [Directory Structure](#directory-structure) - [Workflow](#workflow) - [Error Handling](#error-handling) - [Development Error Messages](#development-error-messages) - [Content Gaps Tracking](#content-gaps-tracking) - [Summary](#summary) - [Best Practices](#best-practices) - [Migration Guide](#migration-guide) - [Troubleshooting](#troubleshooting) This documentation system maintains a single source of truth for content across multiple programming languages (TypeScript, C#, Python), with language-specific fragments embedded within shared templates. ## Quick Start 1. **Create template**: `src/pages/templates/my-guide.mdx` with `` tags 2. **Create fragments**: `src/components/include/my-guide/{lang}.incl.md` with `` sections 3. **Generate**: `npm run generate:docs` or `npm start` ## Architecture ``` src/pages/templates/ ← Templates with tags ↓ (generate-language-docs.ts) src/components/include/ ← Language-specific fragments ({lang}.incl.md) ↓ (Build-time processing) docs/main/{lang}/ ← Auto-generated files (fully resolved) ↓ (Docusaurus) Final rendered page ← Language-specific content only ``` ## Core Concepts [⭐ Skip to Best Practices section](#best-practices) ### Templates (`src/pages/templates/**/*.mdx`) Your source of truth - write common content once with `` placeholders: ```mdx --- title: Getting Started languages: ['typescript', 'python'] # Optional: restrict languages to render documentation for --- # Getting Started Shared content for all languages. The package name is . The context type is . ``` ### Fragments (`src/components/include/**/{lang}.incl.md`) Language-specific content organized by HTML comment sections: ````markdown ```bash npm install @microsoft/teams.ts ``` @microsoft/teams.ts N/A ```` **Fragment Rules:** - `` marks section start - Content continues until next section or file end - Use `N/A` to intentionally skip sections (e.g., when a section is necessary for one or two languages, but not all three) - Full Markdown/MDX supported for block-level content **Directory Mapping:** - Category pages: `src/components/include/{category}/{lang}.incl.md` - Regular pages: `src/components/include/{category}/{filename}/{lang}.incl.md` ### Inline Content For simple, short language-specific text (like API names, method names, or simple phrases), you can use inline content directly in templates without creating separate include files: ```mdx ``` **When to use inline content:** - Short text snippets (API names, method names, parameter names) - Simple differences between languages - Content that's easier to read inline than in separate files **When to use include files:** - Code examples - Complex or multi-line content - Content that benefits from syntax highlighting - Larger documentation sections ## Language Filtering Control which languages generate pages using frontmatter: ```mdx --- title: TypeScript-only Feature languages: ['typescript'] # Restrict rendering to specific languages. If all languages are supported, omit this field. suppressLanguageIncludeWarning: true # Suppress warnings for static content --- ``` **Important**: When a template file has `languages` frontmatter that restricts to specific languages, you should only create include files for those supported languages. For example, if a template is restricted to `['typescript', 'python']`, you should not create a `csharp.incl.md` file - only create `typescript.incl.md` and `python.incl.md`. Useful for: _- Language-specific features _- Migration periods - Framework-specific documentation ## Page Types - **Category pages**: `README.mdx` → `index.mdx` - **Regular pages**: `filename.mdx` → `filename.mdx` - **Hidden pages**: `_filename.mdx` (accessible but hidden from sidebar) ## Directory Structure ``` src/pages/templates/ ├── getting-started/ │ ├── README.mdx # → index.mdx │ ├── quickstart.mdx │ └── _category_.json src/components/include/ ├── getting-started/ # For README.mdx │ ├── typescript.incl.md │ ├── csharp.incl.md │ └── python.incl.md └── getting-started/ └── quickstart/ # For quickstart.mdx ├── typescript.incl.md ├── csharp.incl.md └── python.incl.md docs/main/{lang}/ # Auto-generated (DO NOT EDIT) ├── getting-started/ │ ├── index.mdx │ └── quickstart.mdx ``` ## Workflow 1. **Create template**: `src/pages/templates/my-guide.mdx` 2. **Create fragments**: `src/components/include/my-guide/{lang}.incl.md` 3. **Generate**: `npm run generate:docs` and `npm start` ## Error Handling - **Missing files/sections**: Console warnings (unless language-restricted) - **Empty sections**: Show development error messages - **N/A sections**: Use `N/A` or `not applicable` to intentionally skip sections - **Development mode**: Error messages rendered in browser with markdown formatting - **Production mode**: Clean files without error content - **Page availability**: Tracked in `static/missing-pages.json` - **Content gaps**: Tracked in `scripts/generated/content-gaps.json` and `content-gaps.md` ### Development Error Messages When sections are missing or empty, you'll see helpful error messages: ```markdown **[Dev] Section "install" not found in TypeScript documentation.** Either mark the section explicitly as N/A for intentionally ignored, or fill in documentation. ``` ### Content Gaps Tracking The system now generates development reports to help track missing documentation: - **JSON manifest**: `scripts/generated/content-gaps.json` - Machine-readable data - **Markdown report**: `scripts/generated/content-gaps.md` - Human-readable summary - **Automatic generation**: Created every time docs are generated - **Git ignored**: Reports are not committed to the repository Example content gaps report: ```markdown # Content Gaps Report **8 template(s) have missing sections** ## `in-depth-guides/ai/function-calling.mdx` - **`advanced-features`**: Missing in C#, Python ## Summary - **8** templates with gaps - **27** total missing sections ``` ## Best Practices 1. **Never edit `docs/main/{lang}/` files** - they're auto-generated 2. **Use `languages: ['lang1', 'lang2']` frontmatter** for language-specific pages 3. **Add `suppressLanguageIncludeWarning: true`** for static content pages 4. **Keep section names consistent** across all fragment files 5. **Use `N/A` or `not applicable` explicitly** when a section doesn't apply to a language 6. **Distinguish between missing and intentional**: Empty sections show errors, `N/A` sections are silently skipped 7. **Test all supported languages** before committing 8. **Use block-level tags for rich content**, inline tags for simple text 9. **Add `title`** to frontmatter for proper capitalization and navigation rendering - Use `sidebar_label` to customize sidebar text if needed 10. **Prefix utility pages with underscore** to hide from sidebar 11. **Use correct relative URLs** (no extensions, relative to generated location) 12. **Remove file suffixes from markdown links** - use `[link](../page)` not `[link](../page.md)` or `[link](../README.md)` 13. **Check `missing-pages.json`** after generation to verify restrictions 14. **Review content gaps report** at `scripts/generated/content-gaps.md` to track missing documentation ## Migration Guide 1. **Analyze differences** between existing language versions 2. **Extract common content** → template file 3. **Extract language-specific content** → include files 4. **Create template and includes** following directory structure 5. **Generate and test** all languages 6. **Remove old files** from `docs/{lang}/` ## Troubleshooting - **Translation conflicts**: Regenerate with `npm run generate:docs` - **Missing sidebar labels**: Add `sidebar_label: "Name"` to frontmatter - **Language dropdown issues**: Ensure `missing-pages.json` exists - **Banner not dismissing**: Check browser console for errors - **Unexpected warnings**: Add `suppressLanguageIncludeWarning: true` - **Empty vs N/A sections**: Use `N/A` to intentionally skip; empty sections will show dev errors - **Content gaps tracking**: Check `scripts/generated/content-gaps.md` for missing documentation overview ================================================ FILE: teams.md/README.md ================================================ # Website This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. ### Installation ``` $ npm install ``` ### Local Development ``` $ npm run start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. ### Build ``` $ npm run build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. ### Generate LLM.txt files ``` $ npm run generate:llms ``` This command generates `LLM.txt` files used for SEO purposes. ### Generate docs ``` $ npm run generate:docs ``` This command generates documentation files for the website using the custom LanguageInclude system. To see how to use LanguageInclude, check out the [Language Include documentation](./LANGUAGE-INCLUDE.md). ### Watch docs generation ``` $ npm run generate:docs:watch ``` Watches for changes and regenerates documentation files automatically. ### TypeScript type checking ``` $ npm run typecheck ``` Runs TypeScript type checking on the codebase. ### Prebuild ``` $ npm run prebuild ``` Runs docs generation before building the site. ### Scaffold folders/files ``` $ npm run scaffold -- ``` Scaffolds template/include folders and files. See `src/scripts/scaffold.js` for details. ================================================ FILE: teams.md/docs/main/developer-tools/README.md ================================================ --- sidebar_position: 4 summary: Overview of developer tools in Teams SDK including the CLI for project management and DevTools for testing and debugging agents. --- # Developer Tools One of the main motivations for Teams SDK is to provide excellent tools that simplify and speed up building and testing agents. Because of this, we created the CLI for speedy agent initiation and project management, and DevTools as an accessible way to test your agent's behavior without jumping through deployment hoops. DevTools also provides crucial insight on activity payloads on the Activities page. Learn more about the developer tools that come with Teams SDK. 1. [Teams CLI](./cli) 2. [DevTools](./devtools) ================================================ FILE: teams.md/docs/main/developer-tools/_category_.json ================================================ { "position": 5, "label": "Developer Tools", "collapsed": true } ================================================ FILE: teams.md/docs/main/developer-tools/cli.md ================================================ --- sidebar_position: 1 summary: Comprehensive guide to the Teams CLI tool for creating, managing, and deploying Teams SDK applications with simple command-line operations. Use this when you need to set up a new Teams SDK agent or manage existing ones. --- # Teams CLI The Teams CLI was created with the intent of supporting developers by making common actions simple to implement with just a command line. The CLI overarching features are: | Feature | Description | | ------------- | ---------------------------------------------------------------------------------------------------- | | `new` | Create a new Teams SDK agent by choosing a template that will be ready to run with one command line. | | `config` | Add Microsoft 365 Agents Toolkit configuration files to your existing Teams SDK agent project. | | `environment` | Manage multiple environments (e.g. dev, prod) and their keys for your agent. | :::tip With the CLI, you can enter `npx @microsoft/teams.cli --help` at any command level to access information about the command, tokens, or required arguments. ::: ## Create an agent with one command line ```sh npx @microsoft/teams.cli@latest new ``` The `new` token will create a brand new agent with `app-name` applied as the directory name and project name. :::note The name you choose may have case changes when applied; for example, "My App" would become "my-app' due to the requirements for `package.json` files. ::: :::warning Our Python SDK is currently in Public Preview. As a result, we have the CLI under a feature flag. Please run the below command to enable this language. ::: ```sh $env:ENABLE_EXPERIMENTAL_PYTHON_OPTIONS = 1 ``` ### Optional parameters :::tip Use command line `npx @microsoft/teams.cli --help` to see the latest options for all optional params. ::: | Parameter | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--template` | Ready-to-run templates that serve as a starting point depending on your scenario. Template examples include `ai`, `echo`, `graph`, and more. | | `--start` | Run the agent immediately upon finishing the creation of the project. | | `--toolkit` or `--atk` | Include the configuration files required to run the agent in the debugger via the [Microsoft 365 Agents Toolkit](https://github.com/OfficeDev/teams-toolkit) extension. Options include `basic`, `embed`, and `oauth`, and more may be added in the future. | | `--client-id` | The app client id, if you already have deployed a resource. This will be added to the root `.env` file of the project. | | `--client-secret` | The app client secret, if you already have deployed a resource. This will be added to the root `.env` file of the project. | ## Add Microsoft 365 Agents Toolkit configuration to existing agent An existing project may also have the appropriate Microsoft 365 Agents Toolkit configuration files added by configuration name. ```bash npx @microsoft/teams.cli config add ``` | Configuration | Description | | ------------- | ------------------------------------------------ | | `atk.basic` | Basic Microsoft 365 Agents Toolkit configuration | | `atk.embed` | Configuration for embedded Teams applications | | `atk.oauth` | Configuration for OAuth-enabled applications | Using this command will include - `env`: folders for managing multiple environments - `infra`: files for deployment and provisioning - `.yml` files for tasks, launch, deployment, etc. ## Remove Agents Toolkit configuration files ```bash npx @microsoft/teams.cli config remove ``` ================================================ FILE: teams.md/docs/main/developer-tools/devtools/README.md ================================================ --- sidebar_position: 2 summary: Use DevTools to locally test Teams apps with chat, activity inspection, and card design features. llms: ignore --- # 🛠️ DevTools The developer tools can be used to locally interact with an app to streamline the testing/development process, preventing you from needing to deploy/register the app or expose a public endpoint. ![Screenshot of DevTools showing user prompt 'hello!' and agent response 'you said hello!'.](/screenshots/devtools-echo-chat.png) ## Basic features - **Chat**: Chat with your app the same way you would in Teams without the need for an app id or authentication. This is useful for testing and debugging your app. - **Inspect**: Inspect incoming and outgoing activities on DevTools' Activity page. All activity on your agent logged here, including messages, reactions, and more. - **Cards**: Use the Cards page to visually design and test your cards. Continue on to the next pages to learn more about the features available in DevTools. ================================================ FILE: teams.md/docs/main/developer-tools/devtools/cards.md ================================================ --- sidebar_position: 3 summary: Design and test Adaptive Cards using the DevTools card designer with live preview and JSON editing. llms: ignore --- # 🪪 Cards Use the Cards page to design and test your cards. Then, use the "Attach card" button to add that card as an attachment to your message. By default, the card will be attached in the new message compose box, but you can also attach a card when editing an existing message. ![Card Designer Typescript](https://github.com/microsoft/teams.ts/blob/main/assets/screenshots/adaptive-cards-designer.png?raw=true) ## Using the card designer from Chat Add an attachment to your message by clicking the attachment (paperclip) icon in the compose box. Select "Open card designer" from the dropdown menu, and your card will be added as an attachment to the same message you are composing or editing after you click "Attach card". :::tip DevTools stores your card attachment so you can use it between page navigation (Chat to Cards and back). Only the last card you designed will be stored, and only temporarily, so if you want to save a card, make sure to save the payload to a file or copy it to your clipboard. Also check out the **[Adaptive Cards Designer](https://adaptivecards.microsoft.com/designer)** and [documentation](https://adaptivecards.microsoft.com/designer). ::: ## Pasting Adaptive Card JSON You can also use the "Paste custom JSON" menu option to paste an Adaptive Card JSON payload into the dialog that will renders. This adds the attachment to the message you are composing or editing. Please keep an eye out for a big update coming soon! ================================================ FILE: teams.md/docs/main/developer-tools/devtools/chat.md ================================================ --- sidebar_position: 1 summary: Test chat functionality with your Teams agent using the lightweight DevTools without sideloading into Teams. llms: ignore --- # 💬 Devtools chat Use the lightweight DevTools app that allows you to test chat functionality with your agent without the need to sideload into Teams. This is useful for testing and debugging. ![Empty DevTools chat](https://github.com/microsoft/teams.ts/blob/main/assets/screenshots/devtools_blank_chat.png?raw=true) :::note We plan to add more features to DevTools for a wider variety of testing. Stay tuned! ::: ## Using DevTools Use the Teams SDK dev package as a plugin. ### Installation Add the dev package to your Teams app. ```bash $: npm install @microsoft/teams.dev ``` ### Usage In your app's main file, ensure DevTools plugin is added to the app. :::warning DevTools is not secure and should not be used in production environments. Remove the plugin before deploying your app to production. ::: ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common/logging'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; //... Other imports here const app = new App({ logger: new ConsoleLogger('@samples/echo', { level: 'debug' }), plugins: [new DevtoolsPlugin()], }); ``` When you run your app, for example `npm run dev`, devtools will be running on port 3979 ```bash [nodemon] watching extensions: ts [nodemon] starting `node -r ts-node/register -r dotenv/config ./src/index.ts` [INFO] @samples/echo/http listening on port 3978 🚀 [INFO] @samples/echo/devtools available at http://localhost:3979/devtools ``` :::info If you used the [CLI](../cli) to create an `atk` configuration for your app, DevTools will run on port 3979 when you launch the debugger. ::: When you open the page, you will see a Teams-like chat window and you can immediately interact with your agent. ![Screenshot of DevTools showing user prompt 'hello!' and agent response 'you said hello!'.](/screenshots/devtools-echo-chat.png) ## Teams chat terminology Below is a brief list of the terminology used in the chat window and in Teams: 1. **Compose box**: The area where you type your message and attach files. A newly rendered Chat page will automatically focus on the compose box. 2. **Message actions menu**: The menu that appears when you hover over or focus on a message, offering different actions depending on whether you sent or received the message. ## Chat capabilities The chat window emulates Teams features as closely as possible. Not all Teams features are available in DevTools, but we are working to add more features over time. The following capabilities are available: :::info Accessibility and keyboard navigation is not fully supported in DevTools. Full support for all users is important to us, and we will prioritize acessibility in future preview releases. ::: ### Send messages You can send messages to your agent just like in Teams. In the compose box, type your message and press Enter to send it. ### Attachments Attach up to 10 files to your message using the Attach (paperclip) button. DevTools supports pasting an Adaptive Card JSON or attaching a card from the card designer. See the [Cards page](./cards) for more. :::note Further support for attachments is coming soon! ::: ### Connectivity Check your app's connectivity in three ways: 1. The DevTools banner shows a green badge or 'Connected' text when connected, and red or 'Disconnected' when not. 2. Similarly, the agent's avatar shows a 'Connected' or 'Disconnected' badge. 3. DevTools uses the `ConsoleLogger` that logs connectivity events to the console. Use the browser's console tool to see the logs. ### Message reactions You can react to messages by selecting an emoji in the message actions menu. ![Devtools react to a message](https://github.com/microsoft/teams.ts/blob/main/assets/screenshots/devtools_message_reaction.gif?raw=true) ### Edit your message Edit messages by selecting the Edit (pencil) icon from the message actions menu. Press Enter or the checkmark button to send the edited message, or the Dismiss (X) button to cancel. ### Delete your message Soft delete messages by hovering over your message, pressing the More (ellipsis) button, then the Delete (trash) button. Click "Undo" to restore the message. ### Streaming If your agent is using streaming, DevTools will render messages as a stream with a rainbow border until the stream ends. See the full stream on the [Activities](inspect) page by clicking the Inspect (magnifying glass) button in the message actions menu of the message. ### Feedback Send feedback to your app by clicking the Feedback (thumbs up/down) buttons in the message actions menu and completing the dialog form. :::info The capabilities above will also populate activities to the Activities page, where you can inspect activity payloads and see the full activity history. ::: ### Developer message shortcut For easier debugging, the compose box stores the last five messages sent to the app. Press the Up arrow key to cycle through your message history and resend messages. ![Devtools Up Arrow Feature](https://github.com/microsoft/teams.ts/blob/main/assets/screenshots/devtools_uparrow_feature.gif?raw=true) ================================================ FILE: teams.md/docs/main/developer-tools/devtools/inspect.md ================================================ --- sidebar_position: 2 summary: Inspect all incoming and outgoing activities with your agent using DevTools Activities page for debugging. llms: ignore --- # 🔍 Inspect activities Inspect incoming and outgoing activities on DevTools' Activities page. All interactions with your agent are logged here, including messages, reactions, and more. ![Inspect Activities view](https://github.com/microsoft/teams.ts/blob/main/assets/screenshots/inspect_activity.png?raw=true) ## View all activity The Activities page displays all activities sent to and from your agent in a grid, showing: 1. Activity type (message, reaction, etc.) 2. Direction via down arrow (incoming) or up arrow (outgoing) 3. Conversation type (for now, only personal chat is supported) 4. Sender 5. Timestamp. ### Monitor activity while testing Teams in browser When testing your sideloaded app in the Teams web client, you can monitor activities in DevTools. Once your agent has launched, the agent server will indicate what port DevTools is running on). Open another browser tab and navigate to the DevTools Activities URL. Interact with your agent in the Teams web client and see the activities in DevTools. To learn more, review the [Agents Toolkit](../../teams/configuration/agents-toolkit) page. You can filter activities by type using the filter icon in the Type column header. ### View activity details Selecting an activity in the grid opens a detailed view in Preview mode, showing the full payload as a tree with expandable and collapsible sections. ### View activity JSON Toggle from "Preview" to "JSON" under the "Activity details" header to see the raw JSON payload. ### Copy activity payload Press the Copy button in the top right corner of the Activity details view to copy the payload to your clipboard. ### Inspect activities by ID When in [Chat](chat), you can inspect activities by ID by clicking the magnifying glass icon in the message actions menu. This opens the Activities page with the activity ID filtered in the list, which is useful for inspecting streamed messages, which have multiple activities. To reset the filter, use the filter button in the Type column header and de-select the activity ID to show all activities again. ================================================ FILE: teams.md/docs/main/privacy.md ================================================ --- sidebar_position: 5 summary: Privacy policy detailing data collection practices using Microsoft Clarity for website analytics and user behavior tracking. llms: ignore --- # Privacy Policy We partner with Microsoft Clarity to capture how you use and interact with our website through behavioral metrics, heatmaps, and session replay to improve and market our products/services. Website usage data is captured using first and third-party cookies and other tracking technologies to determine the popularity of products/services and online activity. Additionally, we use this information for site optimization and fraud/security purposes. For more information about how Microsoft collects and uses your data, visit the [Microsoft Privacy Statement](https://www.microsoft.com/en-us/privacy/privacystatement). ================================================ FILE: teams.md/docs/main/teams/README.md ================================================ --- sidebar_position: 3 summary: Overview of Teams-specific features and SDK components that enable agent integration with the Microsoft Teams platform. --- # Teams Integration This section describes Teams-specific features and components of the SDK, helping you understand how your agent integrates with the Microsoft Teams platform. ## Core Concepts When working with Teams, several key components come into play: - **DevTunnel**: Enables local development by creating secure public endpoints - **App Provisioning**: Handles registration and configuration in Teams - **Environment Setup**: Manages Teams-specific configuration files - **App Packaging**: Bundles your agent for Teams deployment ## In This Section 1. [Core Concepts](core-concepts) - Understanding the Teams deployment process and architecture 2. [Teams Manifest](manifest) - Configuring your agent's Teams presence 3. [Microsoft 365 Agents Toolkit](configuration/agents-toolkit) - Using the Agents Toolkit extension for sideloading, deployment, and provisioning 4. [Enabling in M365 Copilot](enabling-in-copilot) - Make your Teams app available in M365 Copilot Each guide provides detailed information about specific aspects of Teams integration, from local development to production deployment. ================================================ FILE: teams.md/docs/main/teams/_category_.json ================================================ { "position": 2, "label": "Teams Integration", "collapsed": false } ================================================ FILE: teams.md/docs/main/teams/app-authentication/README.mdx ================================================ --- sidebar_position: 4 summary: Set up authentication for your Teams bot using client secrets, user managed identities, or federated identity credentials --- import LangLink from '@site/src/components/LangLink'; # App Authentication Setup Your Teams bot needs to authenticate with Azure to send messages. This involves configuring your Azure Bot Service and App Registration correctly. ## Authentication Methods Choose one of the following authentication methods based on your security requirements: 1. **[Client Secret](client-secret)** - Simple password-based authentication using a client secret 2. **[User Managed Identity](user-managed-identity)** - Passwordless authentication using Azure managed identities 3. **[Federated Identity Credentials](federated-identity-credentials)** - Advanced identity federation using managed identities assigned to App Registration Each method has different setup requirements in Azure Portal or Azure CLI. ## After Setup Once you've completed the Azure setup for your chosen authentication method, you'll need to configure your application code. See the App Authentication configuration guide for details on environment variables and code configuration. ## Troubleshooting If you encounter authentication errors, see the [Troubleshooting](troubleshooting) guide for common issues and solutions. ================================================ FILE: teams.md/docs/main/teams/app-authentication/_category_.json ================================================ { "label": "App Authentication Configuration", "position": 3, "collapsed": true } ================================================ FILE: teams.md/docs/main/teams/app-authentication/client-secret.md ================================================ --- sidebar_position: 1 title: Client Secret Setup summary: Set up client secret authentication for your Teams bot in Azure Portal or Azure CLI --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Client Secret Authentication Setup Client Secret authentication is the simplest method, using a password-like secret to authenticate your bot. While easy to set up, secrets need to be rotated periodically and kept secure. ## Prerequisites Before you begin, ensure you have: - An Azure subscription - Permissions to create App Registrations and Azure Bot Services ## Setup Steps ### Step 1: Create Azure Bot with Single Tenant When creating your Azure Bot Service, you must select `Single Tenant` for the `Type of App`. ![Single Tenant Bot Creation](/screenshots/single-tenant-bot.png) ### Step 2: Create Client Secret 1. Navigate to your **App Registration** in the Azure Portal 2. Go to **Certificates and Secrets** 3. Click **New client secret** 4. Add a description and select an expiration period 5. Click **Add** 6. **Important**: Copy the secret value immediately - it won't be shown again ![Secret in Certificates and Secrets](/screenshots/client-secret.png) ```bash # Create a new client secret az ad app credential reset --id $APP_ID --append ``` The command will output the secret value. Save it securely. ## Next Steps After completing the Azure setup, configure your application code with the appropriate environment variables. See the App Authentication Essentials Guide for details. ================================================ FILE: teams.md/docs/main/teams/app-authentication/federated-identity-credentials.md ================================================ --- sidebar_position: 3 title: Federated Identity Credentials Setup summary: Set up Federated Identity Credentials authentication for your Teams bot in Azure Portal or Azure CLI --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Federated Identity Credentials Setup Federated Identity Credentials (FIC) allows you to assign managed identities directly to your App Registration instead of creating a separate User Managed Identity resource. ## Prerequisites Before you begin, ensure you have: - An Azure subscription - Permissions to create App Registrations, Azure Bot Services, and manage identities - A compute resource where your bot will be hosted (App Service, Container App, VM, etc.) - Either a User Managed Identity or the ability to use System Assigned Identity ## Setup Steps ### Step 1: Create Azure Bot with Single Tenant When creating your Azure Bot Service, select `Single Tenant` for the `Type of App`. ![Single Tenant Bot Creation](/screenshots/single-tenant-bot.png) ### Step 2: Configure Federated Credentials Assign managed identities to your App Registration using Federated Credentials. 1. Navigate to your **App Registration** in the Azure Portal 2. Go to **Certificates and Secrets** 3. Select the **Federated Credentials** tab 4. Click **Add credential** 5. Select the federated credential scenario (e.g., "Customer managed keys") 6. Choose the User Managed Identity or configure for System Assigned Identity 7. Complete the required fields and click **Add** ![Federated Identity Creds](/screenshots/fic.png) The identity you select here must also be assigned to the compute resource where your application is hosted. ```bash # Add a federated credential for a user managed identity az ad app federated-credential create \ --id $APP_ID \ --parameters '{ "name": "MyFederatedCredential", "issuer": "https://login.microsoftonline.com/'$TENANT_ID'/v2.0", "subject": "'$MANAGED_IDENTITY_CLIENT_ID'", "audiences": ["api://AzureADTokenExchange"] }' ``` ### Step 3: Assign the Managed Identity to Your Compute Resource The managed identity configured in the federated credential must be assigned to your compute resource. **For User Managed Identity:** 1. Navigate to your compute resource in the Azure Portal 2. Go to **Identity** section in the left menu 3. Select the **User assigned** tab 4. Click **Add** 5. Select the User Managed Identity you configured in the federated credential 6. Click **Add** to confirm **For System Assigned Identity:** 1. Navigate to your compute resource in the Azure Portal 2. Go to **Identity** section in the left menu 3. Select the **System assigned** tab 4. Set **Status** to **On** 5. Click **Save** ```bash # For user managed identity: az webapp identity assign \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --identities $MANAGED_IDENTITY_RESOURCE_ID # For system assigned identity: az webapp identity assign \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP ``` ## Next Steps After completing the Azure setup, configure your application code with the appropriate environment variables. See the App Authentication Essentials Guide for details. ================================================ FILE: teams.md/docs/main/teams/app-authentication/troubleshooting.md ================================================ --- sidebar_position: 4 title: Troubleshooting summary: Common authentication errors and how to resolve them llms: ignore --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Authentication Troubleshooting This guide covers common authentication errors and their solutions. ## Missing Service Principal in the Tenant This error occurs when the application has a single-tenant Azure Bot Service (`msaAppType: 'SingleTenant'`) instance, but your app registration has not yet been linked to a Service Principal in the tenant. ### Error Examples ```sh [ERROR] @teams/app Request failed with status code 401 [ERROR] @teams/app /aaaabbbb-0000-cccc-1111-dddd2222eeee/oauth2/v2.0/token [ERROR] @teams/app { [ERROR] @teams/app "error": "invalid_client", [ERROR] @teams/app "error_description": "AADSTS7000229: The client application 00001111-aaaa-2222-bbbb-3333cccc4444 is missing service principal in the tenant aaaabbbb-0000-cccc-1111-dddd2222eeee. See instructions here: https://go.microsoft.com/fwlink/?linkid=2225119 Trace ID: 0000aaaa-11bb-cccc-dd22-eeeeee333333 Correlation ID: aaaa0000-bb11-2222-33cc-444444dddddd Timestamp: 2025-09-18 01:17:37Z", [ERROR] @teams/app "error_codes": [ [ERROR] @teams/app 7000229 [ERROR] @teams/app ], [ERROR] @teams/app "timestamp": "2025-09-18 01:17:37Z", [ERROR] @teams/app "trace_id": "0000aaaa-11bb-cccc-dd22-eeeeee333333", [ERROR] @teams/app "correlation_id": "aaaa0000-bb11-2222-33cc-444444dddddd", [ERROR] @teams/app "error_uri": "https://login.microsoftonline.com/error?code=7000229" [ERROR] @teams/app } ``` ```sh [ERROR] @teams/app Failed to refresh bot token: Client error '401 Unauthorized' for url 'https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/oauth2/v2.0/token' [ERROR] @teams/app For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 ``` ```sh [ERROR] Echobot Failed to get bot token on app startup. [ERROR] Echobot { [ERROR] Echobot "error": "invalid_client", [ERROR] Echobot "error_description": "AADSTS7000229: The client application 00001111-aaaa-2222-bbbb-3333cccc4444 is missing service principal in the tenant aaaabbbb-0000-cccc-1111-dddd2222eeee. See instructions here: https://go.microsoft.com/fwlink/?linkid=2225119 Trace ID: 0000aaaa-11bb-cccc-dd22-eeeeee333333 Correlation ID: aaaa0000-bb11-2222-33cc-444444dddddd Timestamp: 2025-09-18 02:26:20Z", [ERROR] Echobot "error_codes": [ [ERROR] Echobot 7000229 [ERROR] Echobot ], [ERROR] Echobot "timestamp": "2025-09-18 02:26:20Z", [ERROR] Echobot "trace_id": "0000aaaa-11bb-cccc-dd22-eeeeee333333", [ERROR] Echobot "correlation_id": "aaaa0000-bb11-2222-33cc-444444dddddd", [ERROR] Echobot "error_uri": "https://login.microsoftonline.com/error?code=7000229" [ERROR] Echobot } ``` ### Solution 1. **Sign in to Azure Portal** Go to [https://portal.azure.com](https://portal.azure.com) and log in with your Azure account. 2. **Navigate to App Registrations** In the top search bar, search for **App registrations** and select it. 3. **Search for your application** Use the **BOT_ID** from your environment file: - Local development → `env/.env.local` - Azure deployment → `env/.env.dev` 4. **Check if a Service Principal exists** Open the app registration and verify if a Service Principal is created. If it exists already, you should see an entry for a **Managed Application in your local directory**. ![Screenshot of App Registrations pane in Azure Portal showing value of 'Graphlocal' under the 'Managed application in local directory' field.](/screenshots/existing-service-principal.png) 5. **Create a Service Principal if missing** If it doesn't exist, click **Create Service Principal**. Wait for the page to finish loading. ![Screenshot of App Registrations pane in Azure Portal showing value of 'Create Service Principal' under the 'Managed application in local directory' field.](/screenshots/create-service-principal.png) 6. **Restart your app** Once the Service Principal is created, restart your application. ================================================ FILE: teams.md/docs/main/teams/app-authentication/user-managed-identity.md ================================================ --- sidebar_position: 2 title: User Managed Identity Setup summary: Set up User Managed Identity authentication for your Teams bot in Azure Portal or Azure CLI --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # User Managed Identity Authentication Setup User Managed Identity authentication eliminates the need for secrets or passwords. A managed identity is created alongside your bot and assigned to your compute resource (App Service, Container App, VM, etc.). ## Prerequisites Before you begin, ensure you have: - An Azure subscription - Permissions to create App Registrations, Azure Bot Services, and manage identities - A compute resource where your bot will be hosted (App Service, Container App, VM, etc.) ## Setup Steps ### Step 1: Create Azure Bot with User Managed Identity When creating your Azure Bot Service, select `User Managed Identity` for the `Type of App`. ![User Managed Identity](/screenshots/umi-auth.png) This will automatically create a User Managed Identity resource alongside your bot. ### Step 2: Assign the Managed Identity to Your Compute Resource The User Managed Identity created with your bot must be assigned to the service running your application. 1. Navigate to your compute resource (App Service, Container App, VM, etc.) in the Azure Portal 2. Go to **Identity** section in the left menu 3. Select the **User assigned** tab 4. Click **Add** 5. Select the User Managed Identity that was created with your Azure Bot 6. Click **Add** to confirm ```bash # Assign user managed identity to your compute resource # Example for App Service: az webapp identity assign \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --identities $MANAGED_IDENTITY_RESOURCE_ID # Example for Container App: az containerapp identity assign \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --user-assigned $MANAGED_IDENTITY_RESOURCE_ID ``` ## Next Steps After completing the Azure setup, configure your application code with the appropriate environment variables. See the App Authentication Essentials Guide for details. ================================================ FILE: teams.md/docs/main/teams/configuration/README.md ================================================ # Basic Configuration After learning about the [core concepts](./../core-concepts) on how to enable an application in Teams, this guide will help you configure the underlying resources correctly in Azure. There are two main ways of doing this integration. 1. Using [M365 Agents Toolkit](./agents-toolkit.md) which uses a CLI/VSCode extension to help automate that work for you. 2. [Manually Configuration](./manual-configuration.mdx) if you have a setup that deviates from something simple ================================================ FILE: teams.md/docs/main/teams/configuration/_category_.json ================================================ { "position": 2, "label": "Basic Configuration", "collapsed": true } ================================================ FILE: teams.md/docs/main/teams/configuration/agents-toolkit.md ================================================ --- sidebar_position: 1 summary: Automate Teams app development with Microsoft 365 Agents Toolkit for manifest management, sideloading, and deployment. --- # Microsoft 365 Agents Toolkit Microsoft 365 Agents Toolkit provides a powerful VS Code extension and CLI tool that helps automate important tasks like provisioning and deployment. The project is maintained separately in this repository - [Agents Toolkit GitHub repository](https://github.com/OfficeDev/microsoft-365-agents-toolkit). ## Installing Agents Toolkit Agents Toolkit can be installed as an extension and CLI. Please see the documentation linked below. - [Installing Agents Toolkit extension](https://learn.microsoft.com/microsoftteams/platform/toolkit/install-teams-toolkit) - [Installing Agents Toolkit CLI](https://learn.microsoft.com/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli) :::note - [Teams SDK CLI](../../developer-tools/cli) - helper for Teams SDK. It scaffolds agents, wires in deep Teams features (Adaptive Cards, Conversation History, Memory...etc), and adds all the config files you need while you're coding. - Agents Toolkit CLI - app deployment helper. It sideloads, provisions Azure resources, handles manfiest/tenant plumbing, and keeps your dev, test, and prod environments in sync. ::: ## Official documentation Refer to the official [Microsoft 365 Agents Toolkit documentation](https://learn.microsoft.com/microsoft-365/developer/overview-m365-agents-toolkit?toc=%2Fmicrosoftteams%2Fplatform%2Ftoc.json&bc=%2Fmicrosoftteams%2Fplatform%2Fbreadcrumb%2Ftoc.json) on Microsoft Learn. ## Deployment and provisioning Generally, you can use the toolkit to add required resources to Azure based on your app manifest setup. For more, see [Add cloud resources and API connection](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-resource). ## Resources - [Microsoft 365 Agents Toolkit](https://learn.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals): Extensive documentation covering usage and supported scenarios of Agents Toolkit. - [Teams SDK CLI documentation](../../developer-tools/cli): Instructions on adding Agents Toolkit configurations to your Teams SDK agent. ================================================ FILE: teams.md/docs/main/teams/configuration/manual-configuration.mdx ================================================ --- sidebar_position: 2 summary: Describe how to deploy the Azure Bot Service resource required for Teams bot apps --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Manual Configuration If you prefer manually configuring the resources on Azure, and do not want to follow the automated process, you can follow the following guide. As described in the [Core Concepts](../core-concepts) article, the main things required are an App Registration and an Azure Bot. ## Requirements 1. An Azure subscription 2. Permissions to create Entra ID App registrations. (If you don't have permissions in your tenant, ask your admin to create the App Registration and share the `Application Id`) 3. Permissions to create Azure Bot Service resources 4. (Optional) The [Azure CLI](https://aka.ms/azcli) installed and authenticated to your Azure subscription ### Create the Entra App Registration After a successful App Registration you should have the `TenantId`, `ClientId` and `ClientSecret` values, that you will need later. :::tip We are using Client Secrets authentication here, but it is possible to use other types of authentication. See the [App Authentication](../app-authentication) setup guide for other methods. ::: 1. Navigate to the [Entra Id App Registrations](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) 2. Select `New App Registration` and provide a name. Take note of the assigned `Application Id` (also known as `ClientId`) and `TenantId` 3. Navigate to `Certificates & secrets` and create `New client secret` ```bash botName="My App" appId=$(az ad app create --display-name $botName --sign-in-audience "AzureADMyOrg" --query appId -o tsv) az ad sp create --id $appId appCred=$(az ad app credential reset --id $appId) tenantId=$(echo $appCred | jq -r '.tenant') clientSecret=$(echo $appCred | jq -r '.password') ``` ### Create the Azure Bot Service resource :::tip You can create the Azure Bot Service resource and the Entra App Registration from this screen, and then you will have to create a new client secret. ::: 1. Create or select the resource group where you want to create the Azure Bot Resource 2. In the selected resource group, click Create and search for `bot`. 3. Select the option `Azure Bot`, and click `Create` 4. Provide the Bot handle, eg. `MyBot`, select Data Residency and Pricing tier 1. Under Microsoft App ID, select `Single Tenant` 2. In creation type select `Use existing app registration` and provide the `Application Id` obtained in the previous step To run this script, make sure you initialize the variables `resourceGroup`, `tenantId` and `appId` from the previous steps. ```bash az bot create \ --name $botName \ --app-type SingleTenant \ --appid $appId \ --tenant-id $tenantId \ --resource-group $resourceGroup ``` ### Configure the Azure Bot Service resource Once the Azure Bot Service resource has been created you can configure it. You will need to have set up a public facing endpoint so that messages from your. You can use [DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) if you wish to expose your local servers to public. 1. Under `Settings/Configuration` provide the Message endpoint URL, typically it will look like: `https://myapp.mydomain.com/api/messages` 1. When using DevTunnels for local development, use the devtunnels hosting URL with the relative path `/api/messages` 2. When deploying to a compute instance, such as App Services, Container Apps, or in other Cloud, use the public hostname with the relative path `/api/messages` 2. In `Settings/Channels` enable the `Microsoft Teams` channel. ```bash endpointUrl= az bot update \ --name $botName \ --resource-group $resourceGroup \ --endpoint $endpointUrl az bot msteams create \ --name $botName \ --resource-group $resourceGroup ``` ## Save the credentials to use as configuration ```bash echo "TENANT_ID=$tenantId" > "$botName.env" echo "CLIENT_ID=$appId" >> "$botName.env" echo "CLIENT_SECRET=$clientSecret" >> "$botName.env" ``` ## Resources - [Teams App Publishing overview](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-publish-overview) ================================================ FILE: teams.md/docs/main/teams/core-concepts.md ================================================ --- sidebar_position: 1 summary: Understand Teams app architecture including app registration, Azure Bot Service, DevTunnel, and sideloading processes. --- # Teams Core Concepts When you run your agent on Teams using Microsoft 365 Agents Toolkit, several Teams-specific processes happen behind the scenes. Understanding these components will help you better debug and deploy your agents. Obviously, all these processes can be done manually, but Agents Toolkit automates them for you. ## Basic Flow ```mermaid flowchart LR %% Main actors User([User]) %% Teams section subgraph Teams ["Teams"] TeamsClient["Teams Client"] TeamsBackend["Teams Backend"] end %% Azure section subgraph Azure ["Azure"] AppReg["App Registration"] AzureBot["Azure Bot"] end %% Local Server section subgraph LocalServer ["Local Server"] DevTunnel["DevTunnel"] LocalApp["Your local application"] end %% Deployed Server section subgraph DeployedServer ["Deployed Server"] DeployedApp["Your deployed application"] end %% Define connections User <--> TeamsClient TeamsClient <--> TeamsBackend TeamsBackend <--> AppReg AppReg <--> AzureBot AzureBot --> LocalServer AzureBot --> DeployedServer ``` **Teams** - Teams Client: User-facing agent that interacts with the user. - Teams Backend: Part of your app package; includes a manifest with your app’s client ID. **Azure** - App Registration: Contains a unique client ID and secret for your app. - Azure Bot: Connects your app to Teams; contains a pointer to your HTTPS URL. **Local Server** - Dev Tunnel: Public-facing HTTPS tunnel to expose your local machine. - Local App: Your application running locally; handles events from Teams and sends responses. **Deployed Server** - Deployed App: Your app deployed to the cloud with a public HTTPS endpoint; also interacts with Teams. ## Core Concepts When working with Teams, these are the key concepts. Keep in mind, this is a simplified view of the architecture. - Teams Client: This is the Teams application where users interact with your agent. This can be the desktop app, web app, or mobile app. - Teams Backend: This service handles all the Teams-related operations, including keeping a record of your manifest, and routing messages from your agent to the Azure bot service. - App Registration: This is the registration of your agent in Azure. This Application Registration issues a unique client ID for your application and a client secret. This is used to authenticate your agent application with the Teams backend and other Azure services (including Graph if you are using it). - Azure Bot Service: This is the service that handles all the bot-related operations, including routing messages from Teams to your agent and vice versa. This holds the URL to your agent application. - DevTunnel: This is a service that creates a public facing URL to your locally running application. Azure Bot Service requires that you have a public facing https URL to your agent application. - Local Agent Application: This is your agent application running on your local machine. - Deployed Agent Application: This is your deployed agent which probably has a public facing URL. ## DevTunnel [DevTunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) is a critical component that makes your locally running agent accessible to Teams. When you set up a DevTunnel, it: - Creates a secure public HTTPS endpoint that forwards to your local server - Manages SSL certificates automatically - Routes Teams messages and events to your local agent :::info DevTunnel is only one way of exposing your locally running service to the internet. Other tools like ngrok can also accomplish the same thing. ::: ## Teams App Provisioning Before your agent can interact with Teams, it needs to be properly registered and configured. This step handles creating or updating the App Registration and creating or registering the Azure Bot instance in Azure. ### App Registration - Creates an App ID (i.e. Client ID) in the Teams platform - Sets up a bot registration with the Bot Framework - Creates a client secret that your agent can use to authenticate to be able to send and receive messages. Agents Toolkit will automatically get this value and store it in the `.env` file for you. ### Azure Bot - Creates an Azure Bot resource - Associates the bot with your App Registration - Configures the messaging endpoint to point to your DevTunnel (or public URL if deployed) ## Sideloading Process Sideloading is the process of installing your agent in Teams. You are able to pass in the manifest and icons (zipped up) to the Teams client. Sideloading an application automatically makes that application available to you. You are also able to sideload the application in a Team or a Group chat. In this case, the application will be available to all members of that Team or Group chat. :::warning Sideloading needs to be enabled in your tenant. If this is not the case, then you will need to contact your Teams administrator to enable it. ::: ## Provisioning and Deployment To test your app in Teams, you will at minimum need to have a provisioned Azure bot. You are likely to have other provisionied resources such as storage. Please see the Microsoft Learn [Provision cloud resources](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/provision) documentation for provisioning and deployment using Visual Studio Code and to a container service. ================================================ FILE: teams.md/docs/main/teams/enabling-in-copilot.md ================================================ --- sidebar_position: 6 summary: Learn how to enable your Teams app to work in M365 Copilot by updating the app manifest. --- # Enabling in M365 Copilot If you've built a Teams app or agent and want to make it available in M365 Copilot, you can easily do so by updating your app manifest. This allows users to interact with your agent through Copilot's interface. ## Prerequisites Before enabling your app in Copilot, ensure you have: 1. A working Teams app or agent 2. Completed the app registration process (see "Running in Teams" for your language) 3. Your `BOT_ID` from the app registration ## Updating the Manifest To enable your app in Copilot, add the following configuration to your `manifest.json` file: ```json "copilotAgents": { "customEngineAgents": [ { "type": "bot", "id": "${{BOT_ID}}" } ] } ``` The `BOT_ID` is assigned to your agent after you have registered your application. If you followed the "Running in Teams" guide for your language, this ID should already be available in your environment configuration. ## Location of the Manifest The manifest file is typically located in the `appPackage` folder of your project. If you used the Teams CLI to set up your project, this folder was automatically created for you. ### Example Manifest Structure Here's how the `copilotAgents` section fits into the overall manifest structure: ```json { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", "manifestVersion": "1.19", "version": "1.0.0", "id": "${{APP_ID}}", "developer": { "name": "Your Company", "websiteUrl": "https://www.example.com", "privacyUrl": "https://www.example.com/privacy", "termsOfUseUrl": "https://www.example.com/terms" }, "name": { "short": "Your App Name", "full": "Your Full App Name" }, "description": { "short": "Short description", "full": "Full description of your app" }, "icons": { "outline": "outline.png", "color": "color.png" }, "accentColor": "#FFFFFF", "bots": [ { "botId": "${{BOT_ID}}", "scopes": [ "personal", "team", "groupchat" ], "supportsFiles": false, "isNotificationOnly": false } ], "copilotAgents": { "customEngineAgents": [ { "type": "bot", "id": "${{BOT_ID}}" } ] } } ``` ## Regenerating the App Package After updating the manifest, you need to zip the manifest and icon files into an app package: ### Using Microsoft 365 Agents Toolkit If you're using the Microsoft 365 Agents Toolkit, the app package is automatically regenerated when you build or debug your app. The toolkit will: 1. Read your updated manifest 2. Replace variables like `${{BOT_ID}}` with actual values from your environment 3. Package the manifest and icons into a zip file 4. Deploy the updated package to Teams ### Manual Packaging If you're manually packaging your app: 1. Ensure the `manifest.json` file is updated with the `copilotAgents` section 2. Create a zip file containing: - `manifest.json` - `color.png` (192x192 icon) - `outline.png` (32x32 icon) 3. Upload the zip file to Teams ## Testing in Copilot Once you've updated and redeployed your app: 1. Open M365 Copilot in Teams or at [m365.cloud.microsoft](https://m365.cloud.microsoft/) 2. Your app should now be available as an agent option 3. Interact with your agent through the Copilot interface ## Resources - [Convert Your Declarative Agent for Microsoft 365 Copilot to a Custom Engine Agent](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/convert-declarative-agent) - [Teams app manifest reference](https://learn.microsoft.com/microsoftteams/platform/resources/schema/manifest-schema) ================================================ FILE: teams.md/docs/main/teams/manifest.md ================================================ --- sidebar_position: 5 summary: Learn about Teams app manifest requirements, permissions, and sideloading process for app installation. --- # Teams Manifest Every app or agent installed on Teams requires an app manifest json file, which provides important information and permissions to that app. When sideloading the app, you are required to provide the app manifest via zip which also includes the icons for the app. ## Manifest There are many permissions and details that an app manifest may have added to the `manifest.json`, including the app ID, url, and much more. Please review the comprehensive documentation on the [manifest schema](https://learn.microsoft.com/microsoft-365/extensibility/schema/). ## Sideloading Sideloading is the ability to install and test your app before it is published to your organization's app catalog. For more on sideloading, see [Upload your apps to Teams](https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload). To sideload, ensure the manifest includes all required information (such as the app ID, tenant details, and permissions). Place the manifest and icons at the root of a zip file. For convenient assistance with managing your manifest and automating important functionality like sideloading, deployment, and provisioning, we recommend the [Microsoft 365 Agents Toolkit extension](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/install-teams-toolkit)) and [CLI](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli). Please continue to the [Toolkit documentation](./configuration/agents-toolkit) to learn more. ================================================ FILE: teams.md/docs/main/teams/user-authentication/README.md ================================================ --- sidebar_position: 4 summary: Overview of user authentication in Teams SDK applications, including OAuth, SSO, and secure resource access. --- # User Authentication At times, agents must access secured online resources on behalf of the user, such as checking email, checking flight status, or placing an order. To enable this, the user must authenticate their identity and grant consent for the application to access these resources. This process results in the application receiving a token, which the application can then use to access the permitted resources on the user's behalf. ## How Auth Works When building Teams applications, choosing the right authentication method is crucial for both security and user experience. Teams supports two primary authentication approaches: OAuth and Single Sign-On (SSO). While both methods serve the same fundamental purpose of validating user identity, they differ significantly in their implementation, supported identity providers, and user experience. Understanding these differences is essential for making the right choice for your application. The following table provides a clear comparison between OAuth and SSO authentication methods, highlighting their key differences in terms of identity providers, authentication flows, and user experience. ### Single Sign-On (SSO) Single Sign-On (SSO) in Teams provides a seamless authentication experience by leveraging a user's existing Teams identity. Once a user is logged into Teams, they can access your app without needing to sign in again. The only requirement is a one-time consent from the user, after which your app can securely retrieve their access details from Microsoft Entra ID. This consent is device-agnostic - once granted, users can access your app from any device without additional authentication steps. When an access token expires, the app automatically initiates a token exchange flow. In this process: 1. The Teams client sends an OAuth ID token containing the user's information 2. Your app exchanges this ID token for a new access token with the previously consented scopes 3. This exchange happens silently without requiring user interaction :::tip Always use SSO if you're authenticating the user with Microsoft Entra ID. ::: #### The SSO Signin Flow The SSO signin flow involves several components working together. Here's how it works: 1. User interacts with your bot or message extension app 2. App initiates the signin process 3. If it's the first time: - User is shown a consent form for the requested scopes - Upon consent, Microsoft Entra ID issues an access token (in simple terms) 4. For subsequent interactions: - If token is valid, app uses it directly - If token expires, app silently signs the user in using the token exchange flow This is what the SSO consent form looks like in Teams: ![SSO Consent Form](/screenshots/auth-consent-popup.png) ### OAuth You can use a third-party OAuth Identity Provider (IdP) to authenticate your app users. The app user is registered with the identity provider, which has a trust relationship with your app. When the user attempts to log in, the identity provider validates the app user and provides them with access to your app. Microsoft Entra ID is one such third party OAuth provider. You can use other providers, such as Google, Facebook, GitHub, or any other provider. #### The OAuth Signin Flow The OAuth signin flow involves several components working together. Here's how it works: 1. User interacts with your bot or message extension app 2. App sends a sign-in card with a link to the OAuth provider 3. User clicks the link and is redirected to the provider's authentication page 4. User authenticates and grants consent for requested scopes 5. Provider issues an access token to your app (in simple terms) 6. App uses the token to access services on user's behalf When an access token expires, the user will need to go through the sign-in process again. Unlike SSO, there is no automatic token exchange - the user must explicitly authenticate each time their token expires. #### The OAuth Card This is what the OAuth card looks like in Teams: ![OAuthCard](/screenshots/auth-explicit-signin.png) ## OAuth vs SSO - Head-to-Head Comparison The following table provides a clear comparison between OAuth and SSO authentication methods, highlighting their key differences in terms of identity providers, authentication flows, and user experience. | Feature | OAuth | SSO | | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Identity Provider | Works with any OAuth provider (Microsoft Entra ID, Google, Facebook, GitHub, etc.) | Only works with Microsoft Entra ID | | Authentication Flow | User is sent a card with a sign-in link | If the user has already consented to the requested scopes in the past they will "silently" login through the token exchange flow. Otherwise user is shown a consent form | | User Experience | Requires explicit signin, and consent to scopes | Re-use existing Teams credential. Only requires consent to scopes | | Conversation scopes (`personal`, `groupChat`, `teams`) | `personal` scope only | `personal` scope only | | Azure Configuration differences | Same configuration except `Token Exchange URL` is blank | Same configuration except `Token Exchange URL` is set | ================================================ FILE: teams.md/docs/main/teams/user-authentication/_category_.json ================================================ { "label": "User Authentication Configuration", "collapsed": true, "position": 4, } ================================================ FILE: teams.md/docs/main/teams/user-authentication/sso-setup.mdx ================================================ --- sidebar_position: 1 summary: Describes how to configure SSO in Teams --- # SSO Setup This section describes how to configure the Azure Bot Service (ABS), the Entra App Registration and the Teams manifest to enable Single-Sign-On (SSO) for your Teams app. ## Create the Entra App Registration You need an Entra ID App Registration to configure the OAuth Connection in Azure Bot Service. This can be the same EntraId app registration you used to configure your ABS resource or a new App created just for this purpose. You can create the App Registration from the Azure portal, or the CLI, the next list summarizes the creation steps from the portal. 1. Use the existing App registration, or Create a new App registration from the [Entra Id](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) section in the Azure portal. Now you have an Application Id (also known as Client ID) and a Tenant Id. 2. Provide a name for the app registration, select SingleTenant, and for the Redirect URI select the platform Web and add the value `https://token.botframework.com/.auth/web/redirect` ![Entra new app](/screenshots/entra-new-app.png) 3. Add a new client secret. From `Certificates & secrets`, select `Client secrets` and add `New client secret`. Take note of the secret as you will need the value later on this guide. ![Entra client secret](/screenshots/entra-client-secret.png) 4. Configure the API. From `Expose an API`, Click `Add` to Application ID URI and accept the default value that will look like `api://`. Add the scope `access_as_user` and select who can _consent_. ![Entra oauth scopes](/screenshots/entra-oauth-scopes.png) 5. Authorize the client applications for SSO. To enable the Teams clients, desktop and web, to perform the SSO flow you must add the next client applications to the scope defined before: Teams Desktop `1fec8e78-bce4-4aaf-ab1b-5451cc387264` and Teams Web `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` ![Entra oauth authorize client app](/screenshots/entra-authorize-clientapp.png) ### Configure the Entra App Registration with the CLI ```bash #!/bin/bash az ad app update --id $appId --web-redirect-uris "https://token.botframework.com/.auth/web/redirect" az ad app update --id $appId --identifier-uris "api://$appId" # TODO: add oauthpermission settings and client applications. ``` ## Create the OAuth connection in Azure Bot Service You need to add a new OAuth connection to your Azure Bot Service resource. 1. From the Bot service resource in the Azure Portal, navigate to `Settings/Configuration` and `Add OAuth Connection settings`. 2. Provide a name for your connection e.g. `graph`, and select the _Service Provider_ `Azure Active Directory v2` 3. Populate the `TenantId`/`ClientId`/`ClientSecret` from the values obtained in the previous section steps 2 and 3. Configure the Token Exchange URL with the Application ID URI configured in step 4, and add the Scopes you need e.g. `User.Read` ![ABS OAuth connection](/screenshots/abs-oauth-connection.png) ### Create the OAuth connection using the Azure CLI ```bash #!/bin/bash az bot authsetting create \ --resource-group $resourceGroup \ --name $botName \ -c "graph" \ --client-id $appId \ --client-secret $clientSecret \ --provider-scope-string "User.Read" \ --service "Aadv2" \ --parameters "clientId=$appId" "clientSecret=$clientSecret" "tenantId=$tenantId" "tokenExchangeUrl=api://$appId" ``` ## Configure the App Manifest The Teams application manifest needs to be updated to reflect the settings configure above, with the `Application Id` and `Application ID URI`, if not using `devtunnels`, replace the valid domain with the domain hosting your application. ```json "validDomains": [ "*.devtunnels.ms", "*.botframework.com", ], "webApplicationInfo": { "id": "", "resource": "api://" } ``` ## Troubleshooting If you encounter SSO errors, see the [Troubleshooting](troubleshooting-sso) guide for common issues and solutions. ================================================ FILE: teams.md/docs/main/teams/user-authentication/troubleshooting-sso.mdx ================================================ --- sidebar_position: 2 title: Troubleshooting summary: Common SSO errors and how to resolve them --- import LangLink from '@site/src/components/LangLink'; # SSO Troubleshooting When SSO fails, Teams sends a `signin/failure` invoke activity to your bot with a `code` and `message` describing the error. The SDK's default handler logs a warning with these details. ## Failure codes | Code | Silent | Description | |------|--------|-------------| | `installappfailed` | No | Failed to install the app in the user's personal scope (group chat SSO flow). | | `authrequestfailed` | No | The SSO auth request failed after app installation. | | `installedappnotfound` | Yes | The bot app is not installed for the user or group chat. *(most common)* | | `invokeerror` | Yes | A generic error occurred during the SSO invoke flow. | | `resourcematchfailed` | Yes | The token exchange resource URI on the OAuthCard does not match the Application ID URI in the Entra app registration's "Expose an API" section. *(common)* | | `oauthcardnotvalid` | Yes | The bot's OAuthCard could not be parsed. | | `tokenmissing` | Yes | AAD token acquisition failed. | "Silent" failures produce no user-facing feedback in the Teams client — the user sees nothing and sign-in simply doesn't complete. "Non-silent" failures occur during the group chat SSO flow where the user is shown an install/auth card. :::note The `userconsentrequired` and `interactionrequired` codes are handled by the Teams client via the OAuth card fallback flow and do not typically reach the bot. ::: ## `resourcematchfailed` If you see a warning in your app logs like: > Sign-in failed for user "..." in conversation "...": resourcematchfailed -- Resource match failed This means Teams attempted the SSO token exchange but failed because the token exchange resource URI does not match your Entra app registration. To fix this: 1. **Verify "Expose an API"** in your Entra app registration: the Application ID URI must be set (typically `api://`) 2. **Verify the `access_as_user` scope** is defined under "Expose an API" 3. **Verify pre-authorized client applications** include the Teams Desktop (`1fec8e78-bce4-4aaf-ab1b-5451cc387264`) and Teams Web (`5e3ce6c0-2b1f-4285-8d4b-75ee78787346`) client IDs 4. **Verify the Token Exchange URL** in your Azure Bot OAuth connection matches the Application ID URI exactly 5. **Verify the `webApplicationInfo.resource`** in your Teams app manifest matches the Application ID URI :::tip If you don't need SSO and only want standard OAuth (sign-in button), leave the **Token Exchange URL** blank in your OAuth connection settings. ::: To handle `signin/failure` programmatically in your app, see Handling Sign-In Failures in the User Authentication guide. ================================================ FILE: teams.md/docs/main/welcome.mdx ================================================ --- sidebar_position: 0 summary: Welcome guide to Teams SDK, covering the quickstart process and how to build agents and applications for Microsoft Teams. llms: ignore --- import LangLink from '@site/src/components/LangLink'; # 👋 Welcome Teams SDK is a suite of packages used to develop on Microsoft Teams. Rebuilt from the ground up with improved developer experience in mind, it's never been easier to build powerful agents and applications for the hundreds of millions Microsoft Teams users. ## Quickstart The Teams CLI makes it easy to bootstrap your first agent. Using `npx @microsoft/teams.cli` you can get started using these commands: **TypeScript:** ```bash npx @microsoft/teams.cli@latest new typescript quote-agent --template echo ``` **C#:** ```bash npx @microsoft/teams.cli@latest new csharp quote-agent --template echo ``` **Python:** ```bash npx @microsoft/teams.cli@latest new python quote-agent --template echo ``` ## Overview Microsoft Teams has a robust developer ecosystem with a broad suite of capabilities, now unified via Teams SDK. Whether you are building AI-powered agents, message extensions, embedded web applications, or Graph, Teams SDK has you covered. ## ⭐ What's new? ### Streamlined Developer Experience We’ve refined the developer experience so you can concentrate on building your app’s logic — not wrestling with integrations. ### Effortless Integration We’ve simplified complex integration workflows to help you deliver a richer, more seamless user experience. ### Accelerate Your Workflow Get your application up and running in under 30 seconds with our lightning-fast CLI—so you can spend more time on what really matters. ## 🔎 Navigation Tips We encourage you to use the left sidebar to navigate to your desired section. Can't find what you're searching for? Use the search button above. ================================================ FILE: teams.md/docs/main/why.md ================================================ --- sidebar_position: 1 summary: Explanation of why an SDK is beneficial for building Teams agent applications, covering event handling and proactive messaging patterns. llms: ignore --- # Why An SDK? Before getting into the basics, it's important to understand how an SDK can be helpful when building an agent application. For this, it's a good exercise to understand the basic messaging and event flow of a Teams agent application. --- An agent application is mainly able to do two things: - Listen to events and respond to them - Proactively send messages to the user ```mermaid flowchart LR Teams([Teams]) InputEvents["Input Events"] OutputEvents["Output Events"] Application(["Your application"]) Teams --> InputEvents OutputEvents --> Teams InputEvents --> Application Application --> OutputEvents %% Styling for dark/light compatibility style Teams fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style Application fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff ``` To do this, we already need a few components: 1. A public facing URL to our agent application - This is so that the Teams backend knows where to send messages to when an interesting event happens. 2. A unique identifier for our agent application - Teams doesn't like to pass around this URL everywhere. Instead it hides this information behind a unique ID. This way, if your URL changes, all you need to do is update the URL and keep the ID the same. 3. A way to authenticate to and from the Teams backend - This public facing URL may get hit in many different ways. We need some protections to make sure that the only messages that reach our main application are authenticated. ```mermaid flowchart LR Start("Start") Teams([Teams]) PublicFacingUrl["Public Facing Endpoint"] TeamsURL["Teams Endpoint"] AuthInput["Authentication"] AuthOutput["Authentication"] InputEvents["Input Events"] OutputEvents["Output Events"] Application(["Your application logic"]) Start --> Teams subgraph TeamsEcosystem["Teams Ecosystem"] Teams --> InputEvents end subgraph App["Application with Unique Id"] InputEvents --> PublicFacingUrl PublicFacingUrl --> AuthInput --> Application Application -.-> AuthOutput AuthOutput -.-> OutputEvents end subgraph TeamsEcosystem["Teams Ecosystem"] OutputEvents --> TeamsURL --> Teams end %% Styling for dark/light compatibility style Teams fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style Application fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff ``` Next, once a request is successfully authenticated, there is a _slew_ of possible types of events that can be sent to your agent application. User messages, user reactions, installation events, Adaptive Card actions, dialog actions, and more. All of these get to your application through a single public door - the public facing URL. Not only this, but different types of events may expect a particular type of response back. For example, a message event may expect a text response or an Adaptive Card response, while a reaction event may not expect a response at all. Now, it's possible for your own application to handle all the nuances with these events, but that would be a lot of work, and a lot of boilerplate code. Think, a gigantic switch statement at the very least. ```mermaid block-beta columns 5 block:InputEventsGroup:2 columns 1 InputEvents("Input Events"):1 block:group2:1 columns 2 UserMessaged UserReacted FormSubmitted Etc1["..."] end end space:1 block:AppHandlersGroup:2 columns 1 Handlers("Application Handlers"):1 block:group4:1 columns 2 UserMessageH["User Messaged
handler"] UserReactedH["User Reacted
handler"] FormSubmittedH["Form Submitted
handler"] Etc2["..."] end end InputEventsGroup --> AppHandlersGroup ``` Next, if you wanted to send messages to the user, you would need to make sure each call to the Teams backend is authenticated appropriately for your application. If your application wanted additional data from [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview), you would need to authenticate to that as well. Additionally, if you wanted the _user_ to authenticate and query Graph on their behalf, you would have to set up a solution to do the OAuth flow as well. For these complex flows, Teams offers a solution, but you must adhere to specific protocols and patterns to facilitate them. ```mermaid block-beta columns 4 space:2 block:userAuth:2 columns 4 Graph:1 Github:1 Google:1 Etc["..."]:1 space:4 UserAuth["User OAuth/SSO"]:4 UserAuth-->Graph UserAuth-->Github UserAuth-->Google UserAuth-->Etc end block:events:2 columns 2 blockArrowInput<["InputEvents"]>(right) InputAuthentication["Input Auth"] blockArrowOutput<["OutputEvents"]>(left) OutputAuthentication["Output Auth"] end Application:2 style Application fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff ``` As you can see, there are a lot of moving parts to building a Teams agent application. This is where the SDK comes in. The SDK abstracts away all of the boilerplate code and provides you with a simple interface to work with. It handles all the authentication, routing, and event handling for you, so you can focus on building your application. ================================================ FILE: teams.md/docusaurus.config.ts ================================================ import type * as Preset from '@docusaurus/preset-classic'; import type { Config } from '@docusaurus/types'; import path from 'node:path'; import { themes as prismThemes } from 'prism-react-renderer'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) const baseUrl = '/teams-sdk/'; const config: Config = { title: 'Teams SDK', favicon: 'img/msft-logo-48x48.png', // Set the production url of your site here url: 'https://microsoft.github.io/', // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl, // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'microsoft', // Usually your GitHub org/user name. projectName: baseUrl, // Usually your repo name. onBrokenLinks: 'throw', // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you // may want to replace "en" with "zh-Hans". i18n: { defaultLocale: 'en', locales: ['en'], }, markdown: { mermaid: true, hooks: { onBrokenMarkdownLinks: 'throw', }, }, headTags: [ { tagName: 'link', attributes: { rel: 'llms.txt', href: 'https://microsoft.github.io/teams-sdk/llms_docs/llms.txt' } } ], scripts: [path.join(baseUrl, '/scripts/clarity.js')], presets: [ [ 'classic', { blog: false, docs: { routeBasePath: '/', path: 'docs/main', sidebarPath: './sidebars.ts', sidebarCollapsed: false, editUrl: 'https://github.com/microsoft/teams-sdk/tree/main/teams.md/', // Temporary exclude until generate-LLMs script is fully tested exclude: ['**/LLMs.md'], }, pages: { exclude: ['**/templates/**'], }, theme: { customCss: ['./src/css/custom.css', './src/css/code-blocks.css'], }, } satisfies Preset.Options, ], ], themes: [ '@docusaurus/theme-mermaid', [ require.resolve('@easyops-cn/docusaurus-search-local'), /** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */ { hashed: true, language: ['en'], docsRouteBasePath: ['/', '/typescript', '/csharp', '/python'], indexDocs: true, indexPages: true, highlightSearchTermsOnTargetPage: true, }, ], ], themeConfig: { colorMode: { respectPrefersColorScheme: true, }, navbar: { title: 'Teams SDK', hideOnScroll: true, logo: { alt: 'Teams SDK', src: 'img/teams.png', }, items: [ { href: 'https://github.com/microsoft/teams-sdk/tree/main', position: 'right', className: 'header-github-link', }, ], }, footer: { style: 'dark', links: [ { title: 'Docs', items: [ { label: 'Getting Started', to: '/', }, { label: 'TypeScript', to: '/typescript/getting-started', }, { label: 'C#', to: '/csharp/getting-started', }, { label: 'Python', to: '/python/getting-started', }, { label: 'Privacy policy', to: '/privacy', }, ], }, { title: 'More', items: [ { label: 'GitHub', href: 'https://github.com/microsoft/teams-sdk/tree/main', }, { label: 'Contributing', href: 'https://github.com/microsoft/teams-sdk/blob/main/CONTRIBUTING.md', }, { label: 'Blog', href: 'https://devblogs.microsoft.com/microsoft365dev/announcing-the-updated-teams-ai-library-and-mcp-support/', }, { label: 'Teams agent accelerator templates', href: 'https://microsoft.github.io/teams-agent-accelerator-templates/', }, ], }, ], copyright: `Copyright © ${new Date().getFullYear()} Microsoft Corporation. All rights reserved.`, }, prism: { theme: prismThemes.dracula, darkTheme: prismThemes.vsDark, magicComments: [ { className: 'theme-code-block-highlighted-line', line: 'highlight-next-line', block: { start: 'highlight-start', end: 'highlight-end', }, }, { className: 'code-block-error-line', line: 'highlight-error-line', block: { start: 'highlight-error-start', end: 'highlight-error-end', }, }, { className: 'code-block-success-line', line: 'highlight-success-line', block: { start: 'highlight-success-start', end: 'highlight-success-end', }, }, ], additionalLanguages: [ 'typescript', 'javascript', 'csharp', 'python', 'bash', 'markdown', 'json', ], }, announcementBar: { id: 'teams-sdk-rename', content: 'We have been renamed to Teams SDK! 🎉 🥳', isCloseable: true, backgroundColor: '#515cc6', textColor: '#fff' }, } satisfies Preset.ThemeConfig, }; export default config; ================================================ FILE: teams.md/i18n/en/code.json ================================================ { "theme.ErrorPageContent.title": { "message": "This page crashed.", "description": "The title of the fallback page when the page crashed" }, "theme.BackToTopButton.buttonAriaLabel": { "message": "Scroll back to top", "description": "The ARIA label for the back to top button" }, "theme.blog.archive.title": { "message": "Archive", "description": "The page & hero title of the blog archive page" }, "theme.blog.archive.description": { "message": "Archive", "description": "The page & hero description of the blog archive page" }, "theme.blog.paginator.navAriaLabel": { "message": "Blog list page navigation", "description": "The ARIA label for the blog pagination" }, "theme.blog.paginator.newerEntries": { "message": "Newer entries", "description": "The label used to navigate to the newer blog posts page (previous page)" }, "theme.blog.paginator.olderEntries": { "message": "Older entries", "description": "The label used to navigate to the older blog posts page (next page)" }, "theme.blog.post.paginator.navAriaLabel": { "message": "Blog post page navigation", "description": "The ARIA label for the blog posts pagination" }, "theme.blog.post.paginator.newerPost": { "message": "Newer post", "description": "The blog post button label to navigate to the newer/previous post" }, "theme.blog.post.paginator.olderPost": { "message": "Older post", "description": "The blog post button label to navigate to the older/next post" }, "theme.tags.tagsPageLink": { "message": "View all tags", "description": "The label of the link targeting the tag list page" }, "theme.colorToggle.ariaLabel": { "message": "Switch between dark and light mode (currently {mode})", "description": "The ARIA label for the navbar color mode toggle" }, "theme.colorToggle.ariaLabel.mode.dark": { "message": "dark mode", "description": "The name for the dark color mode" }, "theme.colorToggle.ariaLabel.mode.light": { "message": "light mode", "description": "The name for the light color mode" }, "theme.docs.breadcrumbs.navAriaLabel": { "message": "Breadcrumbs", "description": "The ARIA label for the breadcrumbs" }, "theme.docs.DocCard.categoryDescription.plurals": { "message": "1 item|{count} items", "description": "The default description for a category card in the generated index about how many items this category includes" }, "theme.docs.paginator.navAriaLabel": { "message": "Docs pages", "description": "The ARIA label for the docs pagination" }, "theme.docs.paginator.previous": { "message": "Previous", "description": "The label used to navigate to the previous doc" }, "theme.docs.paginator.next": { "message": "Next", "description": "The label used to navigate to the next doc" }, "theme.docs.tagDocListPageTitle.nDocsTagged": { "message": "One doc tagged|{count} docs tagged", "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" }, "theme.docs.tagDocListPageTitle": { "message": "{nDocsTagged} with \"{tagName}\"", "description": "The title of the page for a docs tag" }, "theme.docs.versionBadge.label": { "message": "Version: {versionLabel}" }, "theme.docs.versions.unreleasedVersionLabel": { "message": "This is unreleased documentation for {siteTitle} {versionLabel} version.", "description": "The label used to tell the user that he's browsing an unreleased doc version" }, "theme.docs.versions.unmaintainedVersionLabel": { "message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.", "description": "The label used to tell the user that he's browsing an unmaintained doc version" }, "theme.docs.versions.latestVersionSuggestionLabel": { "message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).", "description": "The label used to tell the user to check the latest version" }, "theme.docs.versions.latestVersionLinkLabel": { "message": "latest version", "description": "The label used for the latest version suggestion link label" }, "theme.common.editThisPage": { "message": "Edit this page", "description": "The link label to edit the current page" }, "theme.common.headingLinkTitle": { "message": "Direct link to {heading}", "description": "Title for link to heading" }, "theme.lastUpdated.atDate": { "message": " on {date}", "description": "The words used to describe on which date a page has been last updated" }, "theme.lastUpdated.byUser": { "message": " by {user}", "description": "The words used to describe by who the page has been last updated" }, "theme.lastUpdated.lastUpdatedAtBy": { "message": "Last updated{atDate}{byUser}", "description": "The sentence used to display when a page has been last updated, and by who" }, "theme.navbar.mobileVersionsDropdown.label": { "message": "Versions", "description": "The label for the navbar versions dropdown on mobile view" }, "theme.NotFound.title": { "message": "Page Not Found", "description": "The title of the 404 page" }, "theme.admonition.caution": { "message": "caution", "description": "The default label used for the Caution admonition (:::caution)" }, "theme.admonition.danger": { "message": "danger", "description": "The default label used for the Danger admonition (:::danger)" }, "theme.admonition.info": { "message": "info", "description": "The default label used for the Info admonition (:::info)" }, "theme.admonition.note": { "message": "note", "description": "The default label used for the Note admonition (:::note)" }, "theme.admonition.tip": { "message": "tip", "description": "The default label used for the Tip admonition (:::tip)" }, "theme.admonition.warning": { "message": "warning", "description": "The default label used for the Warning admonition (:::warning)" }, "theme.tags.tagsListLabel": { "message": "Tags:", "description": "The label alongside a tag list" }, "theme.AnnouncementBar.closeButtonAriaLabel": { "message": "Close", "description": "The ARIA label for close button of announcement bar" }, "theme.blog.sidebar.navAriaLabel": { "message": "Blog recent posts navigation", "description": "The ARIA label for recent posts in the blog sidebar" }, "theme.CodeBlock.copied": { "message": "Copied", "description": "The copied button label on code blocks" }, "theme.CodeBlock.copyButtonAriaLabel": { "message": "Copy code to clipboard", "description": "The ARIA label for copy code blocks button" }, "theme.CodeBlock.copy": { "message": "Copy", "description": "The copy button label on code blocks" }, "theme.CodeBlock.wordWrapToggle": { "message": "Toggle word wrap", "description": "The title attribute for toggle word wrapping button of code block lines" }, "theme.DocSidebarItem.expandCategoryAriaLabel": { "message": "Expand sidebar category '{label}'", "description": "The ARIA label to expand the sidebar category" }, "theme.DocSidebarItem.collapseCategoryAriaLabel": { "message": "Collapse sidebar category '{label}'", "description": "The ARIA label to collapse the sidebar category" }, "theme.NavBar.navAriaLabel": { "message": "Main", "description": "The ARIA label for the main navigation" }, "theme.navbar.mobileLanguageDropdown.label": { "message": "Languages", "description": "The label for the mobile language switcher dropdown" }, "theme.TOCCollapsible.toggleButtonLabel": { "message": "On this page", "description": "The label used by the button on the collapsible TOC component" }, "theme.NotFound.p1": { "message": "We could not find what you were looking for.", "description": "The first paragraph of the 404 page" }, "theme.NotFound.p2": { "message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.", "description": "The 2nd paragraph of the 404 page" }, "theme.blog.post.readMore": { "message": "Read more", "description": "The label used in blog post item excerpts to link to full blog posts" }, "theme.blog.post.readMoreLabel": { "message": "Read more about {title}", "description": "The ARIA label for the link to full blog posts from excerpts" }, "theme.blog.post.readingTime.plurals": { "message": "One min read|{readingTime} min read", "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" }, "theme.docs.breadcrumbs.home": { "message": "Home page", "description": "The ARIA label for the home page in the breadcrumbs" }, "theme.docs.sidebar.collapseButtonTitle": { "message": "Collapse sidebar", "description": "The title attribute for collapse button of doc sidebar" }, "theme.docs.sidebar.collapseButtonAriaLabel": { "message": "Collapse sidebar", "description": "The title attribute for collapse button of doc sidebar" }, "theme.docs.sidebar.navAriaLabel": { "message": "Docs sidebar", "description": "The ARIA label for the sidebar navigation" }, "theme.docs.sidebar.closeSidebarButtonAriaLabel": { "message": "Close navigation bar", "description": "The ARIA label for close button of mobile sidebar" }, "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { "message": "← Back to main menu", "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" }, "theme.docs.sidebar.toggleSidebarButtonAriaLabel": { "message": "Toggle navigation bar", "description": "The ARIA label for hamburger menu button of mobile navigation" }, "theme.docs.sidebar.expandButtonTitle": { "message": "Expand sidebar", "description": "The ARIA label and title attribute for expand button of doc sidebar" }, "theme.docs.sidebar.expandButtonAriaLabel": { "message": "Expand sidebar", "description": "The ARIA label and title attribute for expand button of doc sidebar" }, "theme.blog.post.plurals": { "message": "One post|{count} posts", "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" }, "theme.blog.tagTitle": { "message": "{nPosts} tagged with \"{tagName}\"", "description": "The title of the page for a blog tag" }, "theme.blog.author.pageTitle": { "message": "{authorName} - {nPosts}", "description": "The title of the page for a blog author" }, "theme.blog.authorsList.pageTitle": { "message": "Authors", "description": "The title of the authors page" }, "theme.blog.authorsList.viewAll": { "message": "View all authors", "description": "The label of the link targeting the blog authors page" }, "theme.blog.author.noPosts": { "message": "This author has not written any posts yet.", "description": "The text for authors with 0 blog post" }, "theme.contentVisibility.unlistedBanner.title": { "message": "Unlisted page", "description": "The unlisted content banner title" }, "theme.contentVisibility.unlistedBanner.message": { "message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.", "description": "The unlisted content banner message" }, "theme.contentVisibility.draftBanner.title": { "message": "Draft page", "description": "The draft content banner title" }, "theme.contentVisibility.draftBanner.message": { "message": "This page is a draft. It will only be visible in dev and be excluded from the production build.", "description": "The draft content banner message" }, "theme.ErrorPageContent.tryAgain": { "message": "Try again", "description": "The label of the button to try again rendering when the React error boundary captures an error" }, "theme.common.skipToMainContent": { "message": "Skip to main content", "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" }, "theme.tags.tagsPageTitle": { "message": "Tags", "description": "The title of the tag list page" } } ================================================ FILE: teams.md/i18n/en/docusaurus-plugin-content-docs/current.json ================================================ { "version.label": { "message": "Next", "description": "The label for version current" } } ================================================ FILE: teams.md/i18n/en/docusaurus-plugin-content-docs-csharp/current.json ================================================ { "version.label": { "message": "Next", "description": "The label for version current" } } ================================================ FILE: teams.md/i18n/en/docusaurus-plugin-content-docs-typescript/current.json ================================================ { "version.label": { "message": "Next", "description": "The label for version current" }, "sidebar.default.category.In Depth Guides": { "message": "In Depth Guides", "description": "The label for category In Depth Guides in sidebar default" }, "sidebar.typescript.category.In Depth Guides": { "message": "In Depth Guides", "description": "The label for category In Depth Guides in sidebar typescript" }, "sidebar.csharp.category.In Depth Guides": { "message": "In Depth Guides", "description": "The label for category In Depth Guides in sidebar csharp" }, "sidebar.python.category.In Depth Guides": { "message": "In Depth Guides", "description": "The label for category In Depth Guides in sidebar python" } } ================================================ FILE: teams.md/i18n/en/docusaurus-theme-classic/footer.json ================================================ { "link.title.Docs": { "message": "Docs", "description": "The title of the footer links column with title=Docs in the footer" }, "link.title.More": { "message": "More", "description": "The title of the footer links column with title=More in the footer" }, "link.item.label.Getting Started": { "message": "Getting Started", "description": "The label of footer link with label=Getting Started linking to /" }, "link.item.label.Blog": { "message": "Blog", "description": "The label of footer link with label=Blog linking to https://devblogs.microsoft.com/microsoft365dev/announcing-the-updated-teams-ai-library-and-mcp-support/" }, "link.item.label.GitHub": { "message": "GitHub", "description": "The label of footer link with label=GitHub linking to https://github.com/microsoft/teams-sdk/tree/main" }, "copyright": { "message": "Copyright © 2025 Microsoft Corporation. All rights reserved.", "description": "The footer copyright" } } ================================================ FILE: teams.md/i18n/en/docusaurus-theme-classic/navbar.json ================================================ { "title": { "message": "Teams SDK", "description": "The title in the navbar" }, "logo.alt": { "message": "Teams SDK", "description": "The alt text of navbar logo" }, "item.label.Typescript": { "message": "Typescript", "description": "Navbar item with label Typescript" }, "item.label.C#": { "message": "C#", "description": "Navbar item with label C#" }, "item.label.Python": { "message": "Python", "description": "Navbar item with label Python" } } ================================================ FILE: teams.md/package.json ================================================ { "name": "teams-md", "version": "0.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", "start": "concurrently --kill-others \"npm run generate:docs:watch\" \"docusaurus start\"", "start:simple": "docusaurus start", "prebuild": "npm run generate:docs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", "generate:docs": "tsx scripts/generate-language-docs.ts", "generate:docs:watch": "tsx scripts/generate-language-docs.ts --watch", "generate:llms": "tsx scripts/generate-llms-txt.ts", "scaffold": "node src/scripts/scaffold.js" }, "dependencies": { "@docusaurus/core": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", "@docusaurus/theme-mermaid": "^3.9.2", "@easyops-cn/docusaurus-search-local": "^0.49.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.2.1", "react-dom": "^19.2.1", "react-json-view-lite": "^2.4.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.9.2", "@docusaurus/tsconfig": "^3.9.2", "@docusaurus/types": "^3.9.2", "@types/chokidar": "^1.7.5", "@types/js-yaml": "^4.0.9", "chokidar": "^3.6.0", "concurrently": "^9.2.1", "js-yaml": "^4.1.1", "json-stable-stringify": "^1.3.0", "tsx": "^4.20.6", "typescript": "~5.6.2" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 3 chrome version", "last 3 firefox version", "last 5 safari version" ] }, "engines": { "node": ">=20.0" } } ================================================ FILE: teams.md/scripts/generate-language-docs.ts ================================================ #!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import * as chokidar from 'chokidar'; import * as yaml from 'js-yaml'; import stringify from 'json-stable-stringify'; import { FrontmatterParser, FRONTMATTER_REGEX } from './lib/frontmatter-parser'; import { LANGUAGES, LANGUAGE_NAMES, type Language, type LanguageAvailabilityMap, } from '../src/constants/languages'; import normalizePath from '../src/utils/normalizePath'; import readFileUtf8Normalized from '../src/utils/readFileUtf8Normalized'; const missingPagesManifest: LanguageAvailabilityMap = {}; const contentGapsManifest: { [templatePath: string]: { [sectionName: string]: Language[] } } = {}; const TEMPLATES_DIR = path.join(__dirname, '..', 'src', 'pages', 'templates'); const FRAGMENTS_DIR = path.join(__dirname, '..', 'src', 'components', 'include'); const DOCS_BASE = path.join(__dirname, '..', 'docs', 'main'); const isProduction = process.env.NODE_ENV === 'production'; // Track all include files that are actually referenced by templates // Languages excluded from a template via frontmatter will not be processed const processedIncludeFiles = new Set(); // For sections in an *.mdx file that is applicable to one or two languages, but not all three. // This is an intentional way of differentiating from missing sections in documentation that haven't been written yet. const NOT_APPLICABLE_REGEX = /^(not applicable|n\/a)\s*$/i; // Notation in *.incl.md files for sections to be added into the *.mdx const SECTION_REGEX = (sectionName: string) => new RegExp(`\\s*([\\s\\S]*?)(?=|$)`, 'i'); // Regex to find LanguageInclude tags (supports both section and content props) const LANGUAGE_INCLUDE_REGEX = //g; const languagePattern = LANGUAGES.join('|'); const LANGUAGE_INCL_FILENAME_REGEX = new RegExp(`^(${languagePattern})\\.incl\\.md$`); /** * Extract a section from markdown content using HTML comment markers * - null: section not found (no matching HTML comment) * - '': section found but marked as N/A (intentionally empty) * - 'EMPTY_SECTION': section found but has no content (should show error in dev) * - string: section content */ function extractSection(markdown: string, sectionName: string): string | null { if (!markdown) { return 'EMPTY_SECTION'; } const match = markdown.match(SECTION_REGEX(sectionName)); // Section not found if (!match) { return null; } const content = match[1].trim(); // Content not applicable if (NOT_APPLICABLE_REGEX.test(content)) { return ''; } // Section exists but has no content if (content === '') { return 'EMPTY_SECTION'; } return content; } /** * Given a template path and language, return the include file path for that language. */ function getIncludeFilePath(templatePath: string, language: Language): string { const relativePath = path.relative(TEMPLATES_DIR, templatePath); const fileName = path.basename(templatePath, '.mdx'); const dirPath = path.dirname(relativePath); // Category files can be either index or README if (fileName === 'index' || fileName === 'README') { return path.join(FRAGMENTS_DIR, dirPath, `${language}.incl.md`); } else { return path.join(FRAGMENTS_DIR, dirPath, fileName, `${language}.incl.md`); } } /** * Given an include file path, return the expected template path. */ function getTemplatePathFromInclude(includePath: string): string { const rel = path.relative(FRAGMENTS_DIR, includePath); const parts = rel.split(path.sep); // foo/bar/lang.incl.md => foo/bar.mdx if (parts.length >= 2) { return path.join(TEMPLATES_DIR, ...parts.slice(0, -1)) + '.mdx'; } else { return '(unknown)'; } } /** * Process LanguageInclude tags in template content and replace with Language components or raw content * - Production mode + target language: generates clean files with only raw content for that language * - Development mode: generates Language components with helpful error messages for missing content */ function processLanguageIncludeTags( templateContent: string, templatePath: string, targetLanguage?: Language ): string { let processedContent = templateContent; let hasLanguageInclude = false; // Replace all LanguageInclude tags processedContent = processedContent.replace( LANGUAGE_INCLUDE_REGEX, (match, sectionName, inlineContent, offset) => { hasLanguageInclude = true; // Determine if this is inline or block based on context const beforeMatch = templateContent.substring(0, offset); // Check if the tag is at the start of a line or after only whitespace (block) // vs. if it's within text content (inline) const lineStart = beforeMatch.lastIndexOf('\n'); const textBeforeOnLine = beforeMatch.substring(lineStart + 1); const isBlock = /^\s*$/.test(textBeforeOnLine); // Handle inline content (content={{...}}) if (inlineContent) { try { // Parse the inline content object const contentObj = JSON.parse(inlineContent); // Production mode with target language if (isProduction && targetLanguage) { const content = contentObj[targetLanguage]; return content || ''; } // Development mode: generate Language components for all languages const languageComponents: string[] = []; for (const lang of LANGUAGES) { const content = contentObj[lang]; if (content) { if (isBlock) { languageComponents.push( `\n\n${content}\n\n` ); } else { languageComponents.push(`${content}`); } } } return languageComponents.join('\n'); } catch (error) { console.warn(`generate-language-docs warning: Error parsing inline content: ${error}`); return match; // Return original tag on error } } // Handle section reference (section="...") // Production mode with target language: only generate for that language if (isProduction && targetLanguage) { const inclPath = getIncludeFilePath(templatePath, targetLanguage); if (!fs.existsSync(inclPath)) { // Skip missing content (prod) return ''; } try { const fileContent = readFileUtf8Normalized(inclPath); const sectionContent = extractSection(fileContent, sectionName); if (sectionContent === null || sectionContent === '' || sectionContent === 'EMPTY_SECTION') { // Skip missing sections (null), intentional N/A content (empty string), or empty sections return ''; } return isBlock ? `${sectionContent}` : sectionContent; } catch (error) { console.warn(`generate-language-docs warning: Error reading ${inclPath}: ${error}`); return ''; } } // Development mode or no target language: generate Language components for all languages // This allows dev-time rendering of messages indicating what include sections are missing. const languageComponents: string[] = []; const languagesToProcess = targetLanguage ? [targetLanguage] : LANGUAGES; for (const lang of languagesToProcess) { const inclPath = getIncludeFilePath(templatePath, lang); let sectionContent: string | null = null; const isLanguageRestricted = shouldGenerateForLanguage(templateContent, lang); if (!isLanguageRestricted) { // Template doesn't target this language; skip continue; } // Only mark as incl file path as used if this template is supposed to generate for this language processedIncludeFiles.add(path.resolve(inclPath)); if (!fs.existsSync(inclPath)) { // File missing for a language the template should support - show error in development if (!isProduction) { const errorMsg = `[DevMode] Documentation file for ${LANGUAGE_NAMES[lang]} not found: ${path.relative(process.cwd(), inclPath)}`; if (isBlock) { languageComponents.push( `\n\n${errorMsg}\n\n` ); } else { languageComponents.push(`${errorMsg}`); } } continue; } try { const fileContent = readFileUtf8Normalized(inclPath); sectionContent = extractSection(fileContent, sectionName); if (sectionContent === null || sectionContent === 'EMPTY_SECTION') { // Section not found (null) or section exists but has no content (EMPTY_SECTION) // Track this gap in the manifest const gapKey = normalizePath(path.relative(TEMPLATES_DIR, templatePath)); if (!contentGapsManifest[gapKey]) { contentGapsManifest[gapKey] = {}; } if (!contentGapsManifest[gapKey][sectionName]) { contentGapsManifest[gapKey][sectionName] = []; } contentGapsManifest[gapKey][sectionName].push(lang); // Both cases show the same error in development, skip in production if (!isProduction) { const errorMsg = `**[Dev] Section "${sectionName}" not found in ${LANGUAGE_NAMES[lang]} documentation.** Either mark the section explicitly as N/A for intentionally ignored, or fill in documentation.`; if (isBlock) { languageComponents.push( `\n\n${errorMsg}\n\n` ); } else { languageComponents.push(`${errorMsg}`); } } continue; } if (sectionContent === '') { // N/A section - intentionally skip any rendering continue; } // Valid content found if (isBlock) { // Block-level: full Language component with markdown processing languageComponents.push( `\n\n${sectionContent}\n\n` ); } else { // Inline: single Language component with just the text content languageComponents.push(`${sectionContent}`); } } catch (error) { console.warn(` Warning: Error reading ${inclPath}: ${error}`); // Generate error component in development if (!isProduction) { const errorMsg = `[Dev] Error reading file: ${error}`; if (isBlock) { languageComponents.push( `\n\n${errorMsg}\n\n` ); } else { languageComponents.push(`${errorMsg}`); } } } } // Return joined Language components (or empty string if no components added) if (isBlock) { // Block: Each language on separate lines return languageComponents.join('\n\n'); } else { // Inline: Return all language components without line breaks to preserve list structure return languageComponents.join(''); } } ); // Add Language component import if we processed any LanguageInclude tags and need Language components const needsLanguageImport = hasLanguageInclude && (!isProduction || !targetLanguage) && !processedContent.includes("import Language from '@site/src/components/Language'"); if (needsLanguageImport) { // Find where to insert the import (after frontmatter if it exists) const frontmatterMatch = processedContent.match(FRONTMATTER_REGEX); const insertPosition = frontmatterMatch ? frontmatterMatch[0].length : 0; const importStatement = "import Language from '@site/src/components/Language';\n\n"; processedContent = processedContent.slice(0, insertPosition) + importStatement + processedContent.slice(insertPosition); } return processedContent; } function deleteInDirectory(dir: string, deletedCount: { count: number }): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { deleteInDirectory(fullPath, deletedCount); // Remove empty directories try { fs.rmdirSync(fullPath); } catch (e) { // Directory not empty and will be cleaned up later } } else if (entry.isFile() && entry.name.endsWith('.mdx')) { fs.unlinkSync(fullPath); deletedCount.count++; } else if (entry.isFile() && entry.name === '_category_.json') { // Delete all category files - they will be regenerated from templates fs.unlinkSync(fullPath); deletedCount.count++; } } } /** * Clean up stale generated files before regeneration * Removes all .mdx files from docs/main/{lang}/ directories */ function cleanGeneratedFiles(): void { console.log('Cleaning up stale generated files...'); let deletedCount = { count: 0 }; for (const lang of LANGUAGES) { const langDir = path.join(DOCS_BASE, lang); if (!fs.existsSync(langDir)) { continue; } // Also remove root-level category file for this language const rootCategoryPath = path.join(langDir, '_category_.json'); if (fs.existsSync(rootCategoryPath)) { fs.unlinkSync(rootCategoryPath); deletedCount.count++; } // Recursively find and delete all .mdx and _category_.json files deleteInDirectory(langDir, deletedCount); } if (deletedCount.count === 0) { console.log('No files to clean'); } else { console.log(` Cleaned up ${deletedCount.count} file(s)\n`); } } /** * Clean up generated files for a specific template * Removes the generated .mdx files for all languages */ function cleanGeneratedFilesForTemplate(templatePath: string): void { const relativePath = path.relative(TEMPLATES_DIR, templatePath); const templateName = path.basename(templatePath); for (const lang of LANGUAGES) { const outputDir = path.join(DOCS_BASE, lang, path.dirname(relativePath)); // Handle both README.mdx -> index.mdx conversion and regular files // Note: README.mdx category templates are converted to index.mdx for generated output const outputFileName = templateName === 'README.mdx' ? 'index.mdx' : templateName; const outputPath = path.join(outputDir, outputFileName); if (fs.existsSync(outputPath)) { fs.unlinkSync(outputPath); console.log(` Removed: docs/main/${lang}/${path.dirname(relativePath)}/${outputFileName}`); } } } /** * Check if a template should be generated for a specific language based on frontmatter */ function shouldGenerateForLanguage(templateContent: string, language: Language): boolean { const { frontmatter } = FrontmatterParser.extract(templateContent); // Check if languages array is specified if (frontmatter.languages && Array.isArray(frontmatter.languages)) { return frontmatter.languages.includes(language); } // No language restriction = generate for all languages return true; } /** * Generate language-specific doc files from templates * Templates in src/components/ (with nested dirs) are copied to docs/main/{lang}/ * Preserves directory structure and processes LanguageInclude tags */ function generateDocsForTemplate(templatePath: string): void { // Calculate relative path from TEMPLATES_DIR to preserve nested structure const relativePath = normalizePath(path.relative(TEMPLATES_DIR, templatePath)); const templateName = path.basename(templatePath); // Read template content (normalize CRLF > LF) const templateContent = readFileUtf8Normalized(templatePath); // Check frontmatter for warning suppression const { frontmatter } = FrontmatterParser.extract(templateContent); const suppressLanguageIncludeWarning = frontmatter.suppressLanguageIncludeWarning === true; // Validate template contained LanguageInclude tags (unless suppressed) if (!suppressLanguageIncludeWarning && !templateContent.includes(' tags.` ); console.warn( ` If the file is intended to be identical for all languages, ignore this warning.\n Suppress this warning by adding suppressLanguageIncludeWarning: true to the file's fronmatter` ); } // Track which languages this page is missing from const pagePath = relativePath .replace(/\.mdx?$/, '') .replace(/README$/, '') .replace(/\/$/, '') || '/'; const missingLanguages: Language[] = []; for (const lang of LANGUAGES) { // Check if this template should be generated for this language if (!shouldGenerateForLanguage(templateContent, lang)) { missingLanguages.push(lang); continue; } const processedContent = processLanguageIncludeTags(templateContent, templatePath, lang); // Extract frontmatter if exists const { frontmatter, hasFrontmatter, content: contentWithoutFrontmatter } = FrontmatterParser.extract(processedContent); let content = contentWithoutFrontmatter; let frontmatterRaw = ''; if (hasFrontmatter && frontmatter && Object.keys(frontmatter).length > 0) { frontmatterRaw = `---\n${yaml.dump(frontmatter)}---\n` } const outputDir = path.join(DOCS_BASE, lang, path.dirname(relativePath)); // Convert README.mdx to index.mdx so it becomes the category page content const outputFileName = templateName === 'README.mdx' ? 'index.mdx' : templateName; const outputPath = path.join(outputDir, outputFileName); // Ensure directory exists if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Build output content let output = ''; // Add frontmatter first if it exists (must be at the very top) if (frontmatter) { output += `${frontmatterRaw}\n`; } // Add auto-generation warning after frontmatter output += `\n\n`; // Add processed content (optimized for production, with Language components for development) output += content; // Write file fs.writeFileSync(outputPath, output, 'utf8'); } // Only add to manifest if some languages are missing if (missingLanguages.length > 0) { missingPagesManifest[pagePath] = missingLanguages; } } /** * Copy category configuration files to all language directories * Copies _category_.json files from template directories to docs/main/{lang}/ * Preserves directory structure */ function copyCategoryFiles(): void { function copyDirectory(sourceDir: string, relativePath: string = ''): void { const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name); const currentRelativePath = path.join(relativePath, entry.name); if (entry.isDirectory()) { copyDirectory(sourcePath, currentRelativePath); } else if (entry.isFile() && entry.name === '_category_.json') { // Copy category file to all language directories with unique keys const categoryContent = JSON.parse(readFileUtf8Normalized(sourcePath)); for (const lang of LANGUAGES) { const targetDir = path.join(DOCS_BASE, lang, relativePath); const targetPath = path.join(targetDir, '_category_.json'); // Ensure target directory exists if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } // Add unique key based on language and relative path const modifiedContent = { ...categoryContent, key: `${lang}-${relativePath.replace(/[/\\]/g, '-') || 'root'}`, }; fs.writeFileSync(targetPath, JSON.stringify(modifiedContent, null, 2) + '\n'); } } } } copyDirectory(TEMPLATES_DIR); } /** * Find all template files in src/components recursively */ function findTemplateFiles(): string[] { if (!fs.existsSync(TEMPLATES_DIR)) { console.error(`Templates directory not found: ${TEMPLATES_DIR}`); return []; } const templates: string[] = []; function searchDirectory(dir: string): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { searchDirectory(fullPath); } else if (entry.isFile() && entry.name.endsWith('.mdx')) { // Skip generation of underscore-prefixed files (utility/unlisted templates) if (entry.name.startsWith('_')) { continue; } templates.push(fullPath); } } } searchDirectory(TEMPLATES_DIR); // Deterministic ordering across platforms templates.sort((a, b) => normalizePath(a).localeCompare(normalizePath(b))); return templates; } /** * Create root-level category files for each language directory * Creates _category_.json files in docs/main/{lang}/ with proper positioning */ function createLanguageRootCategories(): void { // Position values for each language for correct sidebar ordering const languagePositions = { typescript: 2.0, csharp: 2.1, python: 2.2, }; for (const lang of LANGUAGES) { const langDir = path.join(DOCS_BASE, lang); const categoryPath = path.join(langDir, '_category_.json'); // Ensure directory exists if (!fs.existsSync(langDir)) { fs.mkdirSync(langDir, { recursive: true }); } // Create category configuration const categoryConfig = { label: `${LANGUAGE_NAMES[lang]} Guide`, position: languagePositions[lang], collapsible: true, collapsed: false, }; // Write the category file fs.writeFileSync(categoryPath, JSON.stringify(categoryConfig, null, 2), 'utf8'); } } // After all templates are processed, warn about orphaned include files function warnOrphanedIncludeFiles() { const orphanedFiles: Array<{ lang: string; fullPath: string; relTemplate: string }> = []; function scanDir(dir: string) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { scanDir(fullPath); } else if (entry.isFile() && entry.name.endsWith('.incl.md')) { if (!processedIncludeFiles.has(path.resolve(fullPath))) { // Extract language from filename const match = entry.name.match(LANGUAGE_INCL_FILENAME_REGEX); const lang = match ? match[1] : 'unknown'; const templatePath = getTemplatePathFromInclude(fullPath); const relTemplate = normalizePath(path.relative(process.cwd(), templatePath)); orphanedFiles.push({ lang, fullPath, relTemplate }); } } } } scanDir(FRAGMENTS_DIR); if (orphanedFiles.length > 0) { console.warn(`\n[DevMode] Orphaned include files were found. These files are not referenced by any template (possibly due to 'language' frontmatter restrictions):`); orphanedFiles.forEach(({ lang, fullPath, relTemplate }) => { console.warn(` - [${lang}] ${fullPath}\n Template: ${relTemplate}`); }); } } /** * Write the missing pages manifest to static directory */ function writePageManifest(): void { const manifestPath = normalizePath(path.join(__dirname, '..', 'static', 'missing-pages.json')); // Ensure static directory exists const staticDir = path.dirname(manifestPath); if (!fs.existsSync(staticDir)) { fs.mkdirSync(staticDir, { recursive: true }); } // Write compact JSON with only pages that are missing from some languages // Ensure deterministic sorting across platforms using json-stable-stringify fs.writeFileSync(manifestPath, stringify(missingPagesManifest, { space: 2 }) + '\n', 'utf8'); console.log( `\nWrote missing pages manifest to ${path.relative(process.cwd(), manifestPath)} (${Object.keys(missingPagesManifest).length} entries)` ); } /** * Write the content gaps manifest for tracking missing sections */ function writeContentGapsManifest(): void { const generatedDir = normalizePath(path.join(__dirname, 'generated')); const manifestPath = normalizePath(path.join(generatedDir, 'content-gaps.json')); const readmePath = normalizePath(path.join(generatedDir, 'content-gaps.md')); // Ensure generated directory exists if (!fs.existsSync(generatedDir)) { fs.mkdirSync(generatedDir, { recursive: true }); } // Directly stringify manifest (order not important; stable stringify ensures deterministic output anyway) fs.writeFileSync(manifestPath, stringify(contentGapsManifest, { space: 2 }) + '\n', 'utf8'); // Generate human-readable markdown report let markdownContent = `# Content Gaps Report\n\n`; markdownContent += `Generated: ${new Date().toISOString()}\n\n`; markdownContent += `This report tracks missing sections in language-specific documentation.\n\n`; const totalGaps = Object.keys(contentGapsManifest).length; markdownContent += `**${totalGaps} template(s) have missing sections**\n\n`; if (totalGaps > 0) { for (const [templatePath, sections] of Object.entries(contentGapsManifest)) { markdownContent += `## \`${templatePath}\`\n\n`; for (const [sectionName, languages] of Object.entries(sections)) { const langNames = languages.map(lang => LANGUAGE_NAMES[lang]).join(', '); markdownContent += `- **\`${sectionName}\`**: Missing in ${langNames}\n`; } markdownContent += `\n`; } } markdownContent += `## Summary\n\n`; const totalSectionGaps = Object.values(contentGapsManifest) .flatMap(sections => Object.values(sections)) .reduce((total, langs: Language[]) => total + langs.length, 0); markdownContent += `- **${totalGaps}** templates with gaps\n`; markdownContent += `- **${totalSectionGaps}** total missing sections\n`; fs.writeFileSync(readmePath, markdownContent, 'utf8'); console.log( `\nWrote content gaps manifest to ${path.relative(process.cwd(), manifestPath)} (${totalGaps} templates with gaps)` ); console.log(`Generated readable report: ${path.relative(process.cwd(), readmePath)}`); } interface GenerationResult { templatesGenerated: number; contentGapsFound: number; } /** * Generate all docs * @returns Generation result with template count and content gaps found */ function generateAll(): GenerationResult { console.log('generate-language-docs.ts: Generating language-specific documentation...\n'); // Clean up stale files first cleanGeneratedFiles(); // Reset manifests Object.keys(missingPagesManifest).forEach((key) => delete missingPagesManifest[key]); Object.keys(contentGapsManifest).forEach((key) => delete contentGapsManifest[key]); const templates = findTemplateFiles(); if (templates.length === 0) { console.log('No template files found in src/pages/templates/'); return { templatesGenerated: 0, contentGapsFound: 0 }; } templates.forEach(generateDocsForTemplate); // Copy category configuration files copyCategoryFiles(); // Create root-level category files for language directories createLanguageRootCategories(); // Warn about orphaned include files if (!isProduction) { warnOrphanedIncludeFiles(); } // Write the page manifest writePageManifest(); // Write the content gaps manifest writeContentGapsManifest(); console.log(`\nGenerated ${templates.length} template(s) for ${LANGUAGES.length} languages\n`); return { templatesGenerated: templates.length, contentGapsFound: Object.keys(contentGapsManifest).length, }; } /** * Watch mode for development */ function watch(): void { console.log('\nWatching for template and fragment changes...\n'); // Watch templates const templateWatcher = chokidar.watch(path.join(TEMPLATES_DIR, '**/*.mdx'), { persistent: true, ignoreInitial: true, }); templateWatcher.on('add', (filePath: string) => { generateDocsForTemplate(filePath); }); templateWatcher.on('change', (filePath: string) => { generateDocsForTemplate(filePath); }); templateWatcher.on('unlink', (filePath: string) => { cleanGeneratedFilesForTemplate(filePath); }); // Watch Language Include/fragment files (*.incl.md) const inclWatcher = chokidar.watch(path.join(FRAGMENTS_DIR, '**/*.incl.md'), { persistent: true, ignoreInitial: true, }); /** * Handle changes to include files and regenerate corresponding templates * Maps include file paths to their corresponding template files: * - Category pages: src/components/include/{path}/{lang}.incl.md → src/pages/templates/{path}/README.mdx * - Regular pages: src/components/include/{path}/{page}/{lang}.incl.md → src/pages/templates/{path}/{page}.mdx */ const handleInclChange = (filePath: string): void => { const relativePath = path.relative(FRAGMENTS_DIR, filePath); const parts = relativePath.split(path.sep); const langFile = parts.pop(); let templatePath: string; if (langFile && LANGUAGE_INCL_FILENAME_REGEX.test(langFile)) { templatePath = path.join(TEMPLATES_DIR, ...parts, 'README.mdx'); if (!fs.existsSync(templatePath)) { templatePath = path.join(TEMPLATES_DIR, ...parts) + '.mdx'; } } else { templatePath = path.join(TEMPLATES_DIR, ...parts) + '.mdx'; } if (fs.existsSync(templatePath)) { console.log(`\nFragment changed: ${relativePath}`); console.log(`Regenerating template: ${path.relative(TEMPLATES_DIR, templatePath)}`); generateDocsForTemplate(templatePath); } else { console.warn(`\nFragment changed but template not found: ${templatePath}`); console.warn('This might be an orphaned fragment file.'); } }; inclWatcher.on('change', handleInclChange); inclWatcher.on('add', handleInclChange); // Watch category configuration files in templates const categoryWatcher = chokidar.watch(path.join(TEMPLATES_DIR, '**/_category_.json'), { persistent: true, ignoreInitial: true, }); categoryWatcher.on('change', () => { copyCategoryFiles(); }); categoryWatcher.on('add', () => { copyCategoryFiles(); }); } // Main execution if (require.main === module) { const args = process.argv.slice(2); const watchMode = args.includes('--watch') || args.includes('-w'); if (watchMode) { generateAll(); watch(); } else { const result = generateAll(); // In production mode, fail the build if there are content gaps if (isProduction && result.contentGapsFound > 0) { console.error(`\n❌ Build failed: Found ${result.contentGapsFound} template(s) with content gaps in production mode.`); console.error('Please fill in all missing sections or mark them as N/A before deploying.\n'); process.exit(1); } } } export { generateAll, generateDocsForTemplate }; ================================================ FILE: teams.md/scripts/generate-llms-txt.ts ================================================ #!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import { collectFiles, getHierarchicalFiles } from './lib/file-collector'; import { processContent } from './lib/content-processor'; import { FrontmatterParser } from './lib/frontmatter-parser'; import { LANGUAGES, Language, LANGUAGE_NAMES } from '../src/constants/languages'; import readFileUtf8Normalized from '../src/utils/readFileUtf8Normalized'; const LANGUAGE_SPECIFIC_TIPS: Record = { typescript: [ "It's a good idea to build the application using `npm run build` and fix compile time errors to help ensure the app works as expected.", "The SDK uses typescript to help you make the right decisions when using the APIs. You may check type definitions and type checkers to make sure your code is correct." ], python: [ "It's a good idea to run `pyright` to make sure the code is correctly typed and fix any type errors.", ], csharp: [ "It's a good idea to build the application and fix compile time errors to help ensure the app works as expected.", "It is helpful to inspect NuGet packages folder to get exact types for a given namesapce" ] }; const COMMON_OVERALL_SUMMARY = (language: Language) => { const langName = LANGUAGE_NAMES[language]; const tips = LANGUAGE_SPECIFIC_TIPS[language]; const formattedTips = tips.map(tip => `- ${tip}`).join('\n'); return `> Microsoft Teams SDK - A comprehensive framework for building AI-powered Teams applications using ${langName}. Using this SDK, you can easily build and integrate a variety of features in Microsoft Teams by building Agents or Tools. The documentation here helps by giving background information and code samples on how best to do this. IMPORTANT THINGS TO REMEMBER: - This SDK is NOT based off of BotFramework (which the _previous_ version of the Teams SDK was based on). This SDK is a completely new framework. Use this guide to find snippets to drive your decisions. - When scaffolding new applications, using the CLI is a lot simpler and preferred than doing it all by yourself. See the Quickstart guide for that. ${formattedTips} YOU MUST FOLLOW THE ABOVE GUIDANCE.`; }; interface DocusaurusConfig { url: string; baseUrl: string; } interface ProcessedFile { title: string; content: string; frontmatter: { [key: string]: any }; filePath: string; sidebarPosition: number; relativeUrl: string; } interface FileInfo { name: string; title: string; path: string; order: number; } interface FolderStructure { title: string; order: number; path: string; files: FileInfo[]; children: { [key: string]: FolderStructure }; } /** * Reads Docusaurus config to get base URL * @param baseDir - Base directory path * @returns Config object with url and baseUrl */ function getDocusaurusConfig(baseDir: string): DocusaurusConfig { try { // Read the docusaurus.config.ts file const configPath = path.join(baseDir, 'docusaurus.config.ts'); const configContent = readFileUtf8Normalized(configPath); // Extract URL and baseUrl using regex (simple approach) const urlMatch = configContent.match(/url:\s*['"]([^'"]+)['"]/); const baseUrlMatch = configContent.match(/baseUrl\s*=\s*['"]([^'"]+)['"]/) || configContent.match(/baseUrl:\s*['"]([^'"]+)['"]/); const url = urlMatch ? urlMatch[1] : 'https://microsoft.github.io'; const baseUrl = baseUrlMatch ? baseUrlMatch[1] : '/teams-sdk/'; return { url, baseUrl }; } catch (error) { console.warn('⚠️ Could not read Docusaurus config, using defaults'); return { url: 'https://microsoft.github.io', baseUrl: '/teams-sdk/' }; } } /** * Generates llms.txt files for Teams SDK documentation * Creates both small and full versions for TypeScript and C# docs */ async function generateLlmsTxt(): Promise { console.log('🚀 Starting llms.txt generation...'); const baseDir = path.join(__dirname, '..'); const outputDir = path.join(baseDir, 'static', 'llms_docs'); // Get Docusaurus configuration const config = getDocusaurusConfig(baseDir); const cleanUrl = config.url.replace(/\/$/, ''); const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; console.log(`📍 Using base URL: ${cleanUrl}${cleanBaseUrl}`); // Ensure output directory exists if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } try { // Generate llms.txt files for all languages for (const language of LANGUAGES) { const langName = LANGUAGE_NAMES[language]; console.log(`📝 Generating ${langName} llms.txt files...`); await generateLanguageFiles(language, baseDir, outputDir, config); } console.log('✅ Successfully generated all llms.txt files!'); } catch (error) { console.error('❌ Error generating llms.txt files:', error); process.exit(1); } } /** * Generates llms.txt files for a specific language * @param language - 'typescript', 'python', or 'csharp' * @param baseDir - Base directory path * @param outputDir - Output directory path * @param config - Docusaurus config object */ async function generateLanguageFiles(language: Language, baseDir: string, outputDir: string, config: DocusaurusConfig): Promise { // Collect all relevant files const mainFiles: string[] = []; const langFiles = collectFiles(path.join(baseDir, 'docs', 'main', language)); // Process all files to get metadata and file mapping const { processedFiles, fileMapping } = await processAllFiles( [...mainFiles, ...langFiles], baseDir, language ); // Generate individual TXT files for each doc await generateIndividualTxtFiles( processedFiles, outputDir, language, baseDir, config, fileMapping ); // Process content for small version (navigation index) const smallContent = await generateSmallVersionHierarchical( language, baseDir, config, fileMapping ); // Process content for full version (all documentation) const fullContent = await generateFullVersion(language, processedFiles, baseDir); // Write main llms.txt files const smallPath = path.join(outputDir, `llms_${language}.txt`); const fullPath = path.join(outputDir, `llms_${language}_full.txt`); fs.writeFileSync(smallPath, smallContent, 'utf8'); fs.writeFileSync(fullPath, fullContent, 'utf8'); console.log(` ✓ Generated ${path.basename(smallPath)} (${formatBytes(smallContent.length)})`); console.log(` ✓ Generated ${path.basename(fullPath)} (${formatBytes(fullContent.length)})`); console.log(` ✓ Generated ${processedFiles.length} individual .txt files`); } /** * Processes all files and returns structured data * @param allFiles - All file paths to process * @param baseDir - Base directory path * @param language - Language identifier for filtering language-specific files * @returns Object with processedFiles array and fileMapping Map */ async function processAllFiles(allFiles: string[], baseDir: string, language: Language): Promise<{ processedFiles: ProcessedFile[]; fileMapping: Map }> { const processedFiles: ProcessedFile[] = []; // First pass: build file mapping const fileMapping = new Map(); for (const file of allFiles) { // Generate the same filename logic as used in generateIndividualTxtFiles const tempProcessed = await processContent(file, baseDir, false, null, null, language); // Quick pass for title with language filtering if (tempProcessed) { // Only process files that aren't marked to ignore let fileName: string; if (path.basename(file) === 'README.md') { const parentDir = path.basename(path.dirname(file)); fileName = generateSafeFileName(parentDir); } else { fileName = generateSafeFileName(tempProcessed.title || file); } fileMapping.set(file, fileName); } } // Second pass: process files with mapping for link resolution for (const file of allFiles) { const processed = await processContent(file, baseDir, true, fileMapping, null, language); if (processed && (processed.title || processed.content)) { processedFiles.push(processed); } } // Sort by sidebar position, then by title const sortedFiles = processedFiles.sort((a, b) => { const posA = a.sidebarPosition || 999; const posB = b.sidebarPosition || 999; if (posA !== posB) { return posA - posB; } return (a.title || '').localeCompare(b.title || ''); }); return { processedFiles: sortedFiles, fileMapping }; } /** * Generates individual .txt files for each documentation file * @param processedFiles - Array of processed file objects * @param outputDir - Output directory path * @param language - Language identifier * @param baseDir - Base directory path * @param config - Docusaurus config object * @param fileMapping - File mapping for link resolution */ async function generateIndividualTxtFiles( processedFiles: ProcessedFile[], outputDir: string, language: Language, baseDir: string, config: DocusaurusConfig, fileMapping: Map ): Promise { const docsDir = path.join(outputDir, `docs_${language}`); // Clean and recreate docs directory to remove old files if (fs.existsSync(docsDir)) { fs.rmSync(docsDir, { recursive: true }); } fs.mkdirSync(docsDir, { recursive: true }); for (const file of processedFiles) { if (!file.content) continue; // Re-process the file with full URL generation for individual files const reprocessed = await processContent( file.filePath, baseDir, true, fileMapping, config, language ); if (!reprocessed) continue; // Generate safe filename - use folder name for README.md files let fileName: string; if (path.basename(file.filePath) === 'README.md') { // Use parent folder name for README files const parentDir = path.basename(path.dirname(file.filePath)); fileName = generateSafeFileName(parentDir); } else { fileName = generateSafeFileName(file.title || file.filePath); } const outputPath = path.join(docsDir, `${fileName}.txt`); // Use the reprocessed content directly without adding metadata header let txtContent = reprocessed.content || file.content; // Use reprocessed content with full URLs fs.writeFileSync(outputPath, txtContent, 'utf8'); } } /** * Generates the small version of llms.txt (navigation index) * @param language - Language identifier * @param baseDir - Base directory path * @param config - Docusaurus config object * @param fileMapping - Mapping of source files to generated filenames * @returns Generated navigation content */ async function generateSmallVersionHierarchical(language: Language, baseDir: string, config: DocusaurusConfig, fileMapping: Map): Promise { const langName = LANGUAGE_NAMES[language]; // Remove trailing slash from URL and ensure baseUrl starts with slash const cleanUrl = config.url.replace(/\/$/, ''); const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; let content = `# Teams SDK - ${langName} Documentation\n\n`; content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; // Get hierarchical structure const hierarchical = getHierarchicalFiles(baseDir, `main/${language}`); // Add Language-specific Documentation content += renderHierarchicalStructure(hierarchical.language, fullBaseUrl, language, fileMapping, 0); return content; } /** * Renders hierarchical structure with proper indentation * @param structure - Hierarchical structure object * @param baseUrl - Base URL for links * @param language - Language identifier * @param fileMapping - Mapping of source files to generated filenames * @param indentLevel - Current indentation level (0 = section headers, 1+ = bullet points) * @returns Rendered content with proper hierarchy */ function renderHierarchicalStructure(structure: { [key: string]: FolderStructure }, baseUrl: string, language: Language, fileMapping: Map, indentLevel: number = 0): string { let content = ''; // Helper function for folder name formatting function formatFolderName(name: string): string { return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); } // Convert structure to sorted array const folders = Object.entries(structure) .map(([key, value]) => ({ key, ...value })) .sort((a, b) => { const orderA = a.order || 999; const orderB = b.order || 999; if (orderA !== orderB) return orderA - orderB; return a.key.localeCompare(b.key); }); for (const folder of folders) { // Check if this folder has any content (files or children) const hasFiles = folder.files && folder.files.length > 0; const hasChildren = folder.children && Object.keys(folder.children).length > 0; const hasContent = hasFiles || hasChildren; if (hasContent) { // Check if this folder has a README file to make the header clickable const readmeFile = hasFiles ? folder.files.find((f) => path.basename(f.path) === 'README.md') : null; const displayTitle = folder.title && folder.title !== folder.key ? folder.title : formatFolderName(folder.key); // Generate indent for nested folders (use spaces, 2 per level) const indent = ' '.repeat(indentLevel); if (readmeFile) { // Make folder header clickable by linking to the README let folderFileName: string; if (fileMapping && fileMapping.has(readmeFile.path)) { folderFileName = fileMapping.get(readmeFile.path)!; } else { folderFileName = generateSafeFileName(folder.key); } // Use ### header for top-level sections (indentLevel 0), bullet points for nested if (indentLevel === 0) { content += `### [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)\n\n`; } else { content += `${indent}- [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)`; } // Add summary from README if available try { const readmeContent = readFileUtf8Normalized(readmeFile.path); const { frontmatter } = FrontmatterParser.extract(readmeContent); const summary = frontmatter.summary; if (summary) { if (indentLevel === 0) { content += `${summary}\n\n`; } else { content += `: ${summary}`; } } } catch (error) { // Ignore errors reading README summary } if (indentLevel > 0) { content += '\n'; } } else { // No README if (indentLevel === 0) { content += `### ${displayTitle}\n\n`; } else { content += `${indent}- ${displayTitle}\n`; } } // Add files in this folder (sorted by order), excluding README if (hasFiles) { const sortedFiles = [...folder.files] .filter((f) => path.basename(f.path) !== 'README.md') // Exclude README since it's now the header link .sort((a, b) => { const orderA = a.order || 999; const orderB = b.order || 999; if (orderA !== orderB) return orderA - orderB; return a.name.localeCompare(b.name); }); for (const file of sortedFiles) { // Use file mapping to get the correct generated filename let fileName: string; if (fileMapping && fileMapping.has(file.path)) { fileName = fileMapping.get(file.path)!; } else { fileName = generateSafeFileName(file.title || file.name); } const summary = extractSummaryFromFile(file.path); // Files are always indented one level deeper than their parent folder const fileIndent = ' '.repeat(indentLevel + 1); content += `${fileIndent}- [${file.title}](${baseUrl}llms_docs/docs_${language}/${fileName}.txt)`; if (summary) { content += `: ${summary}`; } content += '\n'; } } // Recursively render children with increased indent if (hasChildren) { content += renderHierarchicalStructure(folder.children, baseUrl, language, fileMapping, indentLevel + 1); } // Add spacing after top-level sections if (indentLevel === 0) { content += '\n'; } } } return content; } /** * Extracts summary from a file (cached approach) * @param filePath - Path to the file * @returns File summary or empty string */ function extractSummaryFromFile(filePath: string): string { try { const fileContent = readFileUtf8Normalized(filePath); // First check for summary in frontmatter const { frontmatter, content } = FrontmatterParser.extract(fileContent); const summary = frontmatter.summary; if (summary && typeof summary === 'string') { return summary; } // Remove HTML comments before extracting summary // Generated .mdx files contain AUTO-GENERATED warnings that shouldn't appear in summaries const cleanContent = content.replace(//g, ''); // Fallback to extracting first meaningful paragraph if no summary in frontmatter const paragraphs = cleanContent.split('\n\n'); for (const paragraph of paragraphs) { const clean = paragraph .replace(/#+\s*/g, '') // Remove headers .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold .replace(/\*(.+?)\*/g, '$1') // Remove italic .replace(/`(.+?)`/g, '$1') // Remove inline code .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text .trim(); if (clean.length > 20 && !clean.startsWith('```') && !clean.startsWith('import')) { return clean.length > 100 ? clean.substring(0, 100) + '...' : clean; } } } catch (error) { // Ignore file read errors } return ''; } /** * Generates the full version of llms.txt (complete documentation) * @param language - Language identifier * @param processedFiles - Array of processed file objects * @param baseDir - Base directory path * @returns Generated content */ async function generateFullVersion(language: Language, processedFiles: ProcessedFile[], baseDir: string): Promise { const langName = LANGUAGE_NAMES[language] let content = `# Teams SDK - ${langName} Documentation (Complete)\n\n`; content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; // Group files by section const sections = groupFilesBySection(processedFiles, baseDir); // Process all sections for (const [sectionName, files] of Object.entries(sections)) { if (!files || files.length === 0) continue; content += `## ${formatSectionName(sectionName)}\n\n`; for (const file of files) { if (file.content) { content += `### ${file.title}\n\n${file.content}\n\n---\n\n`; } } } return content; } /** * Groups files by their section based on file path * @param processedFiles - Array of processed file objects * @param baseDir - Base directory path * @returns Grouped files by section */ function groupFilesBySection(processedFiles: ProcessedFile[], baseDir: string): { [key: string]: ProcessedFile[] } { const sections: { [key: string]: ProcessedFile[] } = { main: [], gettingStarted: [], essentials: [], inDepthGuides: [], migrations: [], }; for (const file of processedFiles) { const relativePath = path.relative(path.join(baseDir, 'docs'), file.filePath); if (relativePath.startsWith('main/')) { sections.main.push(file); } else if (relativePath.includes('getting-started/')) { sections.gettingStarted.push(file); } else if (relativePath.includes('essentials/')) { sections.essentials.push(file); } else if (relativePath.includes('in-depth-guides/')) { sections.inDepthGuides.push(file); } else if (relativePath.includes('migrations/')) { sections.migrations.push(file); } else { // Create dynamic section based on directory const parts = relativePath.split('/'); if (parts.length > 1) { const sectionKey = parts[1].replace(/[^a-zA-Z0-9]/g, ''); if (!sections[sectionKey]) { sections[sectionKey] = []; } sections[sectionKey].push(file); } } } // Sort files within each section by sidebar position for (const sectionFiles of Object.values(sections)) { sectionFiles.sort((a, b) => { const posA = a.sidebarPosition || 999; const posB = b.sidebarPosition || 999; if (posA !== posB) { return posA - posB; } return (a.title || '').localeCompare(b.title || ''); }); } return sections; } /** * Generates a safe filename from a title * @param title - Title to convert to filename * @returns Safe filename */ function generateSafeFileName(title: string): string { return ( title .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Remove special characters .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single .replace(/^-|-$/g, '') // Remove leading/trailing hyphens .substring(0, 50) || // Limit length 'untitled' ); } /** * Formats a section name for display * @param sectionName - Section name to format * @returns Formatted section name */ function formatSectionName(sectionName: string): string { const nameMap: { [key: string]: string } = { main: 'Main Documentation', gettingStarted: 'Getting Started', essentials: 'Essentials', inDepthGuides: 'In-Depth Guides', migrations: 'Migrations', }; return ( nameMap[sectionName] || sectionName .replace(/([A-Z])/g, ' $1') // Add spaces before capitals .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter .trim() ); } /** * Formats bytes into human-readable format * @param bytes - Number of bytes * @returns Formatted string */ function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Run the generator if this file is executed directly if (require.main === module) { generateLlmsTxt(); } export { generateLlmsTxt }; ================================================ FILE: teams.md/scripts/lib/content-processor.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { FrontmatterParser } from './frontmatter-parser'; import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; interface ProcessedContent { title: string; content: string; frontmatter: { [key: string]: any }; filePath: string; sidebarPosition: number; relativeUrl: string; } interface ParsedMarkdown { title: string; content: string; frontmatter: { [key: string]: any }; } interface DocusaurusConfig { url: string; baseUrl: string; } /** * Checks if a file should be ignored based on section-wide README filtering * @param filePath - Path to the file to check * @returns True if file should be ignored due to section filtering */ export function shouldIgnoreFileBySection(filePath: string): boolean { // Get the directory path let currentDir = path.dirname(filePath); // Walk up the directory tree looking for README.md or index.mdx files while (currentDir && currentDir !== path.dirname(currentDir)) { const readmePath = path.join(currentDir, 'README.md'); const indexPath = path.join(currentDir, 'index.mdx'); // Check README.md first, then index.mdx const indexFilePath = fs.existsSync(readmePath) ? readmePath : (fs.existsSync(indexPath) ? indexPath : null); if (indexFilePath) { try { const indexContent = readFileUtf8Normalized(indexFilePath); const indexFrontmatter = FrontmatterParser.extract(indexContent).frontmatter; // Only ignore entire section if index file has 'llms: ignore' (not 'ignore-file') if (indexFrontmatter.llms === 'ignore' || indexFrontmatter.llms === false) { return true; } } catch (error) { // Ignore errors reading index file } } // Move up one directory currentDir = path.dirname(currentDir); } return false; } /** * Processes a markdown/MDX file and extracts its content * @param filePath - Path to the file to process * @param baseDir - Base directory for resolving relative paths * @param includeCodeBlocks - Whether to include FileCodeBlock content * @param fileMapping - Optional mapping of source files to generated filenames * @param config - Optional Docusaurus config for full URL generation * @param language - Optional language identifier for URL generation * @returns Processed content with title, content, and metadata */ export async function processContent( filePath: string, baseDir: string, includeCodeBlocks: boolean = false, fileMapping: Map | null = null, config: DocusaurusConfig | null = null, language: string | null = null ): Promise { try { if (!fs.existsSync(filePath)) { console.warn(`⚠️ File not found: ${filePath}`); return { title: '', content: '', frontmatter: {}, filePath, sidebarPosition: 999, relativeUrl: '' }; } const rawContent = readFileUtf8Normalized(filePath); // Check if this file should be excluded from LLM output if (FrontmatterParser.shouldIgnore(rawContent)) { return null; // Return null to indicate this file should be skipped } // Check if language filtering is enabled and if this file is for a different language const { frontmatter: earlyFrontmatter } = FrontmatterParser.extract(rawContent); if (language && earlyFrontmatter.languages) { const fileLanguages = Array.isArray(earlyFrontmatter.languages) ? earlyFrontmatter.languages : [earlyFrontmatter.languages]; if (!fileLanguages.includes(language)) { return null; // Skip this file as it's not for the current language } } const { title, content, frontmatter } = await parseMarkdownContent(rawContent, baseDir, includeCodeBlocks, filePath, fileMapping, config, language); // Check if this file should be ignored due to section-wide filtering if (shouldIgnoreFileBySection(filePath)) { return null; // Return null to indicate this file should be skipped } return { title: title, content: content || '', frontmatter: frontmatter || {}, filePath, sidebarPosition: (frontmatter.sidebar_position as number) || 999, relativeUrl: generateRelativeUrl(filePath, baseDir) }; } catch (error) { console.error(`❌ Error processing file ${filePath}:`, (error as Error).message); throw error; // Re-throw to fail the build } } /** * Parses markdown/MDX content and extracts title and content * @param rawContent - Raw file content * @param baseDir - Base directory for resolving paths * @param includeCodeBlocks - Whether to process FileCodeBlocks * @param filePath - Current file path for resolving relative links * @param fileMapping - Optional mapping of source files to generated filenames * @param config - Optional Docusaurus config for full URL generation * @param language - Optional language identifier for URL generation * @returns Parsed title, content, and frontmatter */ async function parseMarkdownContent( rawContent: string, baseDir: string, includeCodeBlocks: boolean, filePath: string, fileMapping: Map | null, config: DocusaurusConfig | null, language: string | null ): Promise { // Extract and remove frontmatter using enhanced parser with js-yaml const { frontmatter, content: contentWithoutFrontmatter } = FrontmatterParser.extract(rawContent); let content = contentWithoutFrontmatter; // Extract title from first H1 (always required) const h1Match = content.match(/^#\s+(.+)$/m); if (!h1Match) { throw new Error(`No # header found in file: ${filePath}`); } const title = h1Match[1].trim(); // Remove import statements content = content.replace(/^import\s+.*$/gm, ''); // Process FileCodeBlock components if requested if (includeCodeBlocks) { content = await processFileCodeBlocks(content, baseDir); } else { // Remove FileCodeBlock components for small version content = content.replace(//g, '[Code example removed for brevity]'); } // Clean up MDX-specific syntax while preserving markdown content = cleanMdxSyntax(content); // Fix internal relative links content = fixInternalLinks(content, filePath, fileMapping, config, language); // Remove excessive whitespace content = content.replace(/\n{3,}/g, '\n\n').trim(); return { title, content, frontmatter }; } /** * Processes FileCodeBlock components and includes the referenced code * @param content - Content containing FileCodeBlock components * @param baseDir - Base directory for resolving paths * @returns Content with FileCodeBlocks replaced by actual code */ async function processFileCodeBlocks(content: string, baseDir: string): Promise { const fileCodeBlockRegex = /]+)\/>/g; let processedContent = content; let match; while ((match = fileCodeBlockRegex.exec(content)) !== null) { const attributes = parseAttributes(match[1]); const { src, lang } = attributes; if (src) { try { const codeContent = await loadCodeFile(src, baseDir); const codeBlock = `\`\`\`${lang || 'typescript'}\n${codeContent}\n\`\`\``; processedContent = processedContent.replace(match[0], codeBlock); } catch (error) { console.warn(`⚠️ Could not load code file ${src}:`, (error as Error).message); processedContent = processedContent.replace(match[0], `[Code file not found: ${src}]`); } } } return processedContent; } /** * Loads code content from a file referenced by FileCodeBlock * @param src - Source path from FileCodeBlock (e.g., "/generated-snippets/ts/example.ts") * @param baseDir - Base directory * @returns Code content */ async function loadCodeFile(src: string, baseDir: string): Promise { // Convert src path to local file path let filePath: string; if (src.startsWith('/')) { filePath = path.join(baseDir, 'static', src.substring(1)); } else { filePath = path.join(baseDir, 'static', src); } if (fs.existsSync(filePath)) { return readFileUtf8Normalized(filePath).trim(); } else { throw new Error(`File not found: ${filePath}`); } } /** * Parse JSX-style attributes from attribute string * @param attributeString - String containing attributes * @returns Parsed attributes */ function parseAttributes(attributeString: string): { [key: string]: string } { const attributes: { [key: string]: string } = {}; const regex = /(\w+)=["']([^"']+)["']/g; let match; while ((match = regex.exec(attributeString)) !== null) { attributes[match[1]] = match[2]; } return attributes; } /** * Cleans MDX-specific syntax while preserving standard markdown * @param content - Content to clean * @returns Cleaned content */ function cleanMdxSyntax(content: string): string { let cleaned = content; // Remove HTML comments from llms.txt output // Generated .mdx files contain AUTO-GENERATED warnings as HTML comments for developers, // but these developer notes don't belong in AI-friendly documentation files. // Note: Section markers () in .incl.md source files are processed // earlier by generate-language-docs.ts and never appear in generated .mdx files. cleaned = cleaned.replace(//g, ''); // Remove JSX components (except code blocks which are handled separately) cleaned = cleaned.replace(/<\/?[A-Z][^>]*>/g, ''); // Remove empty JSX fragments cleaned = cleaned.replace(/<>\s*<\/>/g, ''); // Remove JSX expressions but keep the content if it's simple text // IMPORTANT: Don't process content inside code blocks (```) const codeBlockRegex = /```[\s\S]*?```/g; const codeBlocks: string[] = []; // Extract code blocks temporarily cleaned = cleaned.replace(codeBlockRegex, (match) => { codeBlocks.push(match); return `___CODE_BLOCK_${codeBlocks.length - 1}___`; }); // Now remove JSX expressions outside code blocks cleaned = cleaned.replace(/\{([^{}]+)\}/g, (match, expr) => { // Keep simple text expressions, remove complex ones if (expr.includes('(') || expr.includes('.') || expr.includes('[')) { return ''; } return expr; }); // Restore code blocks cleaned = cleaned.replace(/___CODE_BLOCK_(\d+)___/g, (match, index) => { return codeBlocks[parseInt(index)]; }); // Clean up multiple empty lines cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n'); return cleaned; } /** * Generates a relative URL for a documentation file * @param filePath - Full path to the file * @param baseDir - Base directory path * @returns Relative URL for the file */ export function generateRelativeUrl(filePath: string, baseDir: string): string { const relativePath = path.relative(path.join(baseDir, 'docs'), filePath); // Convert file path to URL format let url = relativePath .replace(/\\/g, '/') // Convert Windows paths .replace(/\.mdx?$/, '') // Remove .md/.mdx extension .replace(/\/README$/i, '') // Remove /README from end .replace(/\/index$/i, ''); // Remove /index from end // Add leading slash if (!url.startsWith('/')) { url = '/' + url; } // Handle empty URL (root README) if (url === '/') { url = ''; } return url; } /** * Fixes internal relative links to point to generated .txt files * @param content - Content containing markdown links * @param currentFilePath - Path of the current file being processed * @param fileMapping - Optional mapping of source files to generated filenames * @param config - Optional Docusaurus config for full URL generation * @param language - Optional language identifier for URL generation * @returns Content with fixed links */ export function fixInternalLinks( content: string, currentFilePath: string, fileMapping: Map | null, config: DocusaurusConfig | null, language: string | null ): string { // Pattern to match markdown links: [text](link) const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; return content.replace(linkRegex, (match, text, link) => { // Skip external links (http/https/mailto/etc) if (link.startsWith('http') || link.startsWith('mailto') || link.startsWith('#')) { return match; } // Skip absolute paths starting with / if (link.startsWith('/') && !link.startsWith('//')) { return match; } // Handle relative links if (!link.includes('://')) { // Remove any file extensions and anchors const cleanLink = link.split('#')[0].replace(/\.(md|mdx)$/, ''); // If it's just a filename without path separators, it's likely a sibling file if (!cleanLink.includes('/')) { // Try to resolve using file mapping first if (fileMapping) { const currentDir = path.dirname(currentFilePath); const possiblePath = path.join(currentDir, cleanLink + '.md'); const possibleMdxPath = path.join(currentDir, cleanLink + '.mdx'); // Look for the file in the mapping for (const [sourcePath, generatedName] of fileMapping.entries()) { // Check exact path match or basename match if (sourcePath === possiblePath || sourcePath === possibleMdxPath || path.basename(sourcePath, '.md') === cleanLink || path.basename(sourcePath, '.mdx') === cleanLink) { // Generate full URL if config and language are provided if (config && language) { const cleanUrl = config.url.replace(/\/$/, ''); const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${generatedName}.txt)`; } else { return `[${text}](${generatedName}.txt)`; } } } } // Fallback to simple conversion const safeFileName = cleanLink .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .substring(0, 50) || 'untitled'; // Generate full URL if config and language are provided if (config && language) { const cleanUrl = config.url.replace(/\/$/, ''); const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${safeFileName}.txt)`; } else { return `[${text}](${safeFileName}.txt)`; } } } // Return original link if we can't process it return match; }); } /** * Extracts a summary from content (first paragraph or section) * @param content - Content to summarize * @param maxLength - Maximum length of summary * @returns Content summary */ export function extractSummary(content: string, maxLength: number = 200): string { // Remove markdown formatting for summary let summary = content .replace(/#+\s*/g, '') // Remove headers .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold .replace(/\*(.+?)\*/g, '$1') // Remove italic .replace(/`(.+?)`/g, '$1') // Remove inline code .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text .trim(); // Get first paragraph const firstParagraph = summary.split('\n\n')[0]; // Truncate if too long if (firstParagraph.length > maxLength) { return firstParagraph.substring(0, maxLength).trim() + '...'; } return firstParagraph; } ================================================ FILE: teams.md/scripts/lib/file-collector.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import { FrontmatterParser } from './frontmatter-parser'; import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; interface FileInfo { name: string; title: string; path: string; order: number; } interface FolderStructure { title: string; order: number; path: string; files: FileInfo[]; children: { [key: string]: FolderStructure }; } interface HierarchicalFiles { language: { [key: string]: FolderStructure }; } /** * Recursively collects all markdown and MDX files from a directory * @param dirPath - Directory path to search * @param extensions - File extensions to collect (default: ['.md', '.mdx']) * @returns Array of file paths */ export function collectFiles(dirPath: string, extensions: string[] = ['.md', '.mdx']): string[] { const files: string[] = []; if (!fs.existsSync(dirPath)) { console.warn(`⚠️ Directory not found: ${dirPath}`); return files; } /** * Recursively traverse directory * @param currentPath - Current directory path */ function traverse(currentPath: string): void { const items = fs.readdirSync(currentPath, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(currentPath, item.name); if (item.isDirectory()) { // Skip common directories that don't contain docs if (!shouldSkipDirectory(item.name)) { traverse(fullPath); } } else if (item.isFile()) { const ext = path.extname(item.name).toLowerCase(); if (extensions.includes(ext)) { files.push(fullPath); } } } } traverse(dirPath); // Sort files for consistent ordering return files.sort(); } /** * Determines if a directory should be skipped during traversal * @param dirName - Directory name * @returns True if directory should be skipped */ export function shouldSkipDirectory(dirName: string): boolean { const skipDirs = [ 'node_modules', '.git', 'build', 'dist', '.next', '.docusaurus', 'coverage', '__pycache__' ]; return skipDirs.includes(dirName) || dirName.startsWith('.'); } /** * Gets files organized hierarchically based on folder structure and sidebar_position * @param basePath - Base documentation path * @param language - Language identifier ('typescript' or 'csharp' or 'python') * @returns Hierarchically organized file structure */ export function getHierarchicalFiles(basePath: string, language: string): HierarchicalFiles { const langPath = path.join(basePath, 'docs', language); const structure: HierarchicalFiles = { language: buildHierarchicalStructure(langPath) }; return structure; } /** * Builds hierarchical structure for a directory * @param rootPath - Root directory path * @returns Hierarchical structure with folders and files */ export function buildHierarchicalStructure(rootPath: string): { [key: string]: FolderStructure } { if (!fs.existsSync(rootPath)) { return {}; } const structure: { [key: string]: FolderStructure } = {}; const seenTitles = new Map(); // Track titles and their file paths for duplicate detection /** * Recursively processes a directory * @param dirPath - Current directory path * @param currentLevel - Current level in the structure */ function processDirectory(dirPath: string, currentLevel: { files: FileInfo[]; children: { [key: string]: FolderStructure } }): void { const items = fs.readdirSync(dirPath, { withFileTypes: true }); // Collect folders and files separately const folders: FileInfo[] = []; const files: FileInfo[] = []; for (const item of items) { const fullPath = path.join(dirPath, item.name); if (item.isDirectory() && !shouldSkipDirectory(item.name)) { // Process subdirectory // Check for both README.md and index.mdx const readmePath = path.join(fullPath, 'README.md'); const indexPath = path.join(fullPath, 'index.mdx'); let folderOrder = 999; let folderTitle = item.name; // Get folder ordering from README.md or index.mdx const indexFilePath = fs.existsSync(readmePath) ? readmePath : (fs.existsSync(indexPath) ? indexPath : null); if (indexFilePath) { try { const indexContent = readFileUtf8Normalized(indexFilePath); const { frontmatter, content } = FrontmatterParser.extract(indexContent); // Skip this entire folder if index file is marked to ignore if (frontmatter.llms === 'ignore' || frontmatter.llms === false) { continue; // Skip this folder entirely } // If index file is marked ignore-file, skip just the file but process folder // (folderOrder and folderTitle will use defaults) folderOrder = (frontmatter.sidebar_position as number) || 999; // Extract title from frontmatter or first # header if (frontmatter.title || frontmatter.sidebar_label) { folderTitle = (frontmatter.title || frontmatter.sidebar_label) as string; } else { // Extract from first # header const headerMatch = content.match(/^#\s+(.+)$/m); if (headerMatch) { folderTitle = headerMatch[1].trim(); } } } catch (error) { // Ignore errors reading index file } } folders.push({ name: item.name, title: folderTitle, path: fullPath, order: folderOrder }); } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) { // Process file let fileOrder = 999; let fileTitle = item.name; try { const fileContent = readFileUtf8Normalized(fullPath); const { frontmatter, content } = FrontmatterParser.extract(fileContent); // Skip this file if marked to ignore (including ignore-file) if (frontmatter.llms === 'ignore' || frontmatter.llms === 'ignore-file' || frontmatter.llms === false) { continue; // Skip this file } fileOrder = (frontmatter.sidebar_position as number) || 999; // Extract title from first # header const headerMatch = content.match(/^#\s+(.+)$/m); if (headerMatch) { fileTitle = headerMatch[1].trim(); } // Check for duplicate titles if (seenTitles.has(fileTitle)) { const existingPath = seenTitles.get(fileTitle)!; throw new Error( `Duplicate title found: "${fileTitle}"\n` + ` First occurrence: ${existingPath}\n` + ` Duplicate found in: ${fullPath}` ); } seenTitles.set(fileTitle, fullPath); } catch (error) { // Re-throw to fail the build throw error; } files.push({ name: item.name, title: fileTitle, path: fullPath, order: fileOrder }); } } // Sort files by order and add to current level files.sort((a, b) => { if (a.order !== b.order) return a.order - b.order; return a.name.localeCompare(b.name); }); if (files.length > 0) { if (!currentLevel.files) currentLevel.files = []; currentLevel.files.push(...files); } // Sort folders by order and process each one folders.sort((a, b) => { if (a.order !== b.order) return a.order - b.order; return a.name.localeCompare(b.name); }); if (!currentLevel.children) currentLevel.children = {}; for (const folder of folders) { currentLevel.children[folder.name] = { title: folder.title, order: folder.order, path: folder.path, files: [], children: {} }; // Recursively process subdirectory processDirectory(folder.path, currentLevel.children[folder.name]); } } // Create a temporary wrapper to handle the root properly const tempWrapper: { files: FileInfo[]; children: { [key: string]: FolderStructure } } = { files: [], children: {} }; processDirectory(rootPath, tempWrapper); // Return the children (which contain the actual folder structure) return tempWrapper.children; } /** * Gets priority files for small version generation * @param organized - Organized file structure from getOrganizedFiles * @returns Priority files for small version */ export function getPriorityFiles(organized: any): string[] { const priorityFiles: string[] = []; // Add welcome/overview files priorityFiles.push(...organized.main.welcome); // Add key team concepts const keyTeamFiles = organized.main.teams.filter((file: string) => file.includes('core-concepts') || file.includes('README.md') ); priorityFiles.push(...keyTeamFiles); // Add getting started files priorityFiles.push(...organized.language.gettingStarted); // Add essential README files const essentialReadmes = organized.language.essentials.filter((file: string) => file.includes('README.md') || file.includes('app-basics') ); priorityFiles.push(...essentialReadmes); return priorityFiles; } ================================================ FILE: teams.md/scripts/lib/frontmatter-parser.ts ================================================ import * as fs from 'fs'; import * as yaml from 'js-yaml'; import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; /** * Regular expression to match YAML frontmatter at the start of a file * Matches content between --- delimiters */ export const FRONTMATTER_REGEX = /^---\s*\r?\n([\s\S]*?)\r?\n---/; interface FrontmatterData { [key: string]: string | number | boolean; } interface ExtractResult { frontmatter: FrontmatterData; content: string; hasFrontmatter: boolean; } /** * Parser for YAML frontmatter in markdown/MDX files */ export class FrontmatterParser { /** * Extracts and parses frontmatter from content using js-yaml * @param content - Raw file content * @returns Object with parsed frontmatter and content without frontmatter */ static extract(content: string): ExtractResult { const match = content.match(FRONTMATTER_REGEX); if (!match) { return { frontmatter: {}, content: content, hasFrontmatter: false }; } try { // Use js-yaml for robust YAML parsing instead of custom parser const frontmatter = yaml.load(match[1]) as FrontmatterData || {}; const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); return { frontmatter, content: contentWithoutFrontmatter, hasFrontmatter: true }; } catch (error) { console.warn(`Warning: Error parsing frontmatter with js-yaml, falling back to simple parser:`, error); // Fallback to simple parser const frontmatter = this.parseSimple(match[1]); const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); return { frontmatter, content: contentWithoutFrontmatter, hasFrontmatter: true }; } } /** * Parses frontmatter text into an object (simple parser - kept for fallback) * @param frontmatterText - Raw frontmatter content (without --- delimiters) * @returns Parsed frontmatter object */ static parseSimple(frontmatterText: string): FrontmatterData { const frontmatter: FrontmatterData = {}; const lines = frontmatterText.split('\n'); for (const line of lines) { const match = line.match(/^(\w+):\s*(.+)$/); if (!match) continue; const key = match[1]; let value: string | number | boolean = match[2].trim(); value = this._parseValue(value); frontmatter[key] = value; } return frontmatter; } /** * Extracts frontmatter from a file * @param filePath - Path to the file * @returns Parsed frontmatter or null if file doesn't exist */ static extractFromFile(filePath: string): ExtractResult | null { if (!fs.existsSync(filePath)) { return null; } try { const content = readFileUtf8Normalized(filePath); return this.extract(content); } catch (error) { console.warn(`⚠️ Error reading frontmatter from ${filePath}:`, (error as Error).message); return null; } } /** * Gets a specific property from frontmatter * @param content - File content or path * @param propertyName - Property to extract * @param defaultValue - Default value if property not found * @returns Property value or default */ static getProperty(content: string, propertyName: string, defaultValue?: T): T | undefined { const result = typeof content === 'string' && fs.existsSync(content) ? this.extractFromFile(content) : this.extract(content); if (!result) { return defaultValue; } const { frontmatter } = result; return frontmatter[propertyName] !== undefined ? (frontmatter[propertyName] as T) : defaultValue; } /** * Checks if a file or content should be ignored based on frontmatter * @param content - File content or path * @returns True if should be ignored */ static shouldIgnore(content: string): boolean { const result = typeof content === 'string' && fs.existsSync(content) ? this.extractFromFile(content) : this.extract(content); if (!result) { return false; } const { frontmatter } = result; const llmsValue = frontmatter.llms; return llmsValue === 'ignore' || llmsValue === 'ignore-file' || llmsValue === false; } /** * Parses a value from frontmatter, converting types as needed * @private * @param value - Raw value string * @returns Parsed value (string, number, boolean) */ private static _parseValue(value: string): string | number | boolean { // Remove surrounding quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { return value.slice(1, -1); } // Parse booleans if (value === 'true') return true; if (value === 'false') return false; // Parse integers if (/^\d+$/.test(value)) { return parseInt(value, 10); } // Return as string return value; } } ================================================ FILE: teams.md/sidebars.ts ================================================ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; export default { default: [{ type: 'autogenerated', dirName: '.' }], } satisfies SidebarsConfig; ================================================ FILE: teams.md/src/components/FileCodeBlock.tsx ================================================ import { useEffect, useState, PropsWithChildren } from "react"; import CodeBlock from "@theme/CodeBlock"; import useBaseUrl from "@docusaurus/useBaseUrl"; export type FileCodeBlockParams = { readonly src: string; readonly lang?: string; }; export default function FileCodeBlock({ src, lang }: PropsWithChildren) { const [code, setCode] = useState(); const url = useBaseUrl(src); useEffect(() => { (async () => { try { const res = await fetch(url); if (!res.ok || res.status != 200) { throw new Error(`failed to load file code block with status "${res.status}"`); } const blob = await res.blob(); const data = await blob.text(); setCode(data.trim()); } catch (err) { console.error('failed to load file code block', err); } })(); }, [src]); return ( {code} ); } ================================================ FILE: teams.md/src/components/LangLink.tsx ================================================ import React, { type PropsWithChildren } from 'react'; import Link from '@docusaurus/Link'; import { useLanguagePreference } from '../hooks/useLanguagePreference'; interface LangLinkProps { /** Path relative to the language root, e.g. "essentials/app-authentication" */ to: string; } /** * A link component that automatically prefixes the path with the user's * selected language preference. * * Usage: Link text * Renders: Link text */ export default function LangLink({ to, children }: PropsWithChildren) { const { language } = useLanguagePreference(); return {children}; } ================================================ FILE: teams.md/src/components/Language.tsx ================================================ import { PropsWithChildren } from 'react'; import { useLocation } from '@docusaurus/router'; import { type Language } from '../constants/languages'; export type LanguageProps = { readonly language: Language; }; // Component for inserting language-specific content onto a page. export default function Language({ language, children }: PropsWithChildren) { const location = useLocation(); // Only render if current path matches language if (!location.pathname.includes(`/${language}/`)) { return null; } return <>{children}; } ================================================ FILE: teams.md/src/components/LanguageBanner.tsx ================================================ import React, { useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { LANGUAGE_NAMES, type Language } from '../constants/languages'; interface LanguageBannerProps { targetLanguage: Language; baseUrl: string; onDismiss: () => void; } export default function LanguageBanner({ targetLanguage, baseUrl, onDismiss, }: LanguageBannerProps) { const actionRef = useRef(null); useEffect(() => { actionRef.current?.focus(); }, []); const handleGoToLanguageDocs = () => { window.location.href = `${baseUrl}${targetLanguage}/`; }; // Find the container to render the banner in - prefer docMainContainer for proper width const container = document.querySelector('.docMainContainer') || document.querySelector('main') || document.querySelector('.main-wrapper') || document.querySelector('[role="main"]') || document.body; return createPortal(
This page isn't available for {LANGUAGE_NAMES[targetLanguage]}. Go to the {LANGUAGE_NAMES[targetLanguage]} documentation home instead?
, container ); } ================================================ FILE: teams.md/src/components/LanguageDropdown.tsx ================================================ import React, { useEffect, useState, useRef } from 'react'; import { useLocation } from '@docusaurus/router'; import { useHistory } from '@docusaurus/router'; import useBaseUrl from '@docusaurus/useBaseUrl'; import { LANGUAGE_NAMES, LANGUAGES, type Language } from '../constants/languages'; import { useLanguagePreference } from '../hooks/useLanguagePreference'; import { getLanguageFromPath, getLanguageFromPathStrict, replaceLanguageInPath, getManifestPathFromUrl, } from '../utils/languageUtils'; import { isPageAvailableForLanguage } from '../utils/pageAvailability'; import LanguageBanner from './LanguageBanner'; interface LanguageDropdownProps { // Docusaurus navbar item props className?: string; position?: 'left' | 'right'; // Catch for default docusaurus navbar item props [key: string]: any; } export default function LanguageDropdown(props: LanguageDropdownProps) { const { className = '', position, ...otherProps } = props; const location = useLocation(); const history = useHistory(); const baseUrl = useBaseUrl('/'); const { language, setLanguage } = useLanguagePreference(); const buttonRef = useRef(null); const listRef = useRef(null); const skipNextSync = useRef(false); const [bannerRender, setBannerRender] = useState<{ language: Language } | null>(null); const [focusedIndex, setFocusedIndex] = useState(null); const [isOpen, setIsOpen] = useState(false); const languagesArray = Object.entries(LANGUAGE_NAMES); const getLanguageIndex = (lang: Language): number => { return Math.max( 0, languagesArray.findIndex(([l]) => l === lang) ); }; const handleLanguageChange = async (newLanguage: Language) => { if (newLanguage === language) { return; } skipNextSync.current = true; setIsOpen(false); setLanguage(newLanguage); const currentPath = location.pathname; const currentLanguage = getLanguageFromPathStrict(currentPath, baseUrl); // Navigate to parallel newLanguage's page if we're currently in a language-specific page if (currentLanguage && LANGUAGES.includes(currentLanguage)) { const targetUrl = replaceLanguageInPath(currentPath, baseUrl, newLanguage); if (targetUrl === currentPath) { history.push(`${baseUrl}${newLanguage}/`); } else { // Convert URL path to manifest path format const manifestPath = getManifestPathFromUrl(currentPath, baseUrl); try { // Debugging output console.log('[LanguageDropdown] Checking page availability:', { manifestPath, newLanguage, }); // Check if target page exists for the new language using availability data const pageExists = await isPageAvailableForLanguage(manifestPath, newLanguage); console.log('[LanguageDropdown] Page exists result:', { manifestPath, newLanguage, pageExists, }); if (pageExists) { history.push(targetUrl); } else { // Page doesn't exist, show redirect banner instead of navigating setBannerRender({ language: newLanguage }); } } catch (error) { console.error('Error checking page availability:', error, { manifestPath, newLanguage }); // On error, just navigate normally as fallback history.push(targetUrl); } } } // No navigation necessary if on a page from `/main/` folder (general content) }; const handleBannerDismiss = () => { // Get the current URL language context to restore const currentUrlLanguage = getLanguageFromPath(location.pathname, baseUrl); // Restore language preference to match the current URL context if (currentUrlLanguage && LANGUAGES.includes(currentUrlLanguage)) { setLanguage(currentUrlLanguage); } setBannerRender(null); }; const openListbox = () => { setIsOpen(true); setFocusedIndex(getLanguageIndex(language)); // Move focus to listbox setTimeout(() => listRef.current?.focus(), 0); }; const closeListbox = () => { setIsOpen(false); setFocusedIndex(null); buttonRef.current?.focus(); }; const handleButtonClick = () => { if (isOpen) { closeListbox(); } else { openListbox(); } }; const handleBlur: React.FocusEventHandler = (e) => { const next = e.relatedTarget as Node | null; if (!next || !listRef.current?.contains(next)) { // If focus didn’t move into the listbox, close setIsOpen(false); } }; const handleButtonKeyDown: React.KeyboardEventHandler = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!isOpen) { openListbox(); } } }; // Keyboard navigation handling const handleListKeyDown = (e: React.KeyboardEvent) => { if (!isOpen) { return; } switch (e.key) { case 'Home': e.preventDefault(); setFocusedIndex(0); break; case 'End': e.preventDefault(); setFocusedIndex(languagesArray.length - 1); break; case 'ArrowDown': e.preventDefault(); setFocusedIndex((prev) => { if (prev === null) { return 0; } const nextIndex = prev + 1; if (nextIndex >= languagesArray.length) { return languagesArray.length - 1; } return nextIndex; }); break; case 'ArrowUp': e.preventDefault(); setFocusedIndex((prev) => { if (prev === null) { return languagesArray.length - 1; } const nextIndex = prev - 1; if (nextIndex < 0) { return 0; } return nextIndex; }); break; case 'Tab': closeListbox(); break; case 'Escape': e.preventDefault(); closeListbox(); break; case 'Enter': e.preventDefault(); if (focusedIndex !== null) { const [lang] = languagesArray[focusedIndex]; handleLanguageChange(lang as Language); closeListbox(); } break; } }; const handleOptionClick = (language: Language) => { handleLanguageChange(language); closeListbox(); }; const handleOptionMouseMove = (index: number) => { setFocusedIndex(index); }; // Sync language preference with URL context whenever location changes useEffect(() => { // prevent URL sync from overriding user update if (skipNextSync.current) { skipNextSync.current = false; return; } const currentUrlLanguage = getLanguageFromPathStrict(location.pathname, baseUrl); if ( currentUrlLanguage && currentUrlLanguage !== language && !document.title.includes('Page Not Found') ) { const manifestPath = getManifestPathFromUrl(location.pathname, baseUrl); const syncFunction = async () => { try { const pageExists = await isPageAvailableForLanguage(manifestPath, currentUrlLanguage); if (pageExists && language !== currentUrlLanguage) { setLanguage(currentUrlLanguage); } } catch { if (language !== currentUrlLanguage) { setLanguage(currentUrlLanguage); } } }; syncFunction(); } }, [location.pathname, baseUrl]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( isOpen && !buttonRef.current?.contains(event.target as Node) && !listRef.current?.contains(event.target as Node) ) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen]); return (
{isOpen && (
    {Object.entries(LANGUAGE_NAMES).map(([lang, label], index) => (
  • handleOptionClick(lang as Language)} onMouseMove={() => handleOptionMouseMove(index)} > {label}
  • ))}
)} {bannerRender && ( )}
); } ================================================ FILE: teams.md/src/components/include/essentials/api/csharp.incl.md ================================================ `app.Api` | Area | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | | `Meetings` | Gives your application access to meeting details and participant information via `GetByIdAsync` and `GetParticipantAsync` | | `Teams` | Gives your application access to team or channel details | `Api` import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```csharp app.OnMessage(async (context, cancellationToken) => { var members = await context.Api.Conversations.Members.Get(context.Conversation.Id); }); ``` ```csharp app.OnMeetingStart(async (context, cancellationToken) => { var meetingId = context.Activity.Value.Id; var tenantId = context.Activity.ChannelData?.Tenant?.Id; var userId = context.Activity.From?.AadObjectId; if (meetingId != null && tenantId != null && userId != null) { var participant = await context.Api.Meetings.GetParticipantAsync(meetingId, userId, tenantId); // participant.Meeting?.Role — "Organizer", "Presenter", "Attendee" // participant.Meeting?.InMeeting — true/false } }); ``` ```csharp var members = await app.Api.Conversations.Members.Get("..."); ``` ================================================ FILE: teams.md/src/components/include/essentials/api/python.incl.md ================================================ `app.api` | Area | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | | `meetings` | Gives your application access to meeting details and participant information via `get_by_id` and `get_participant` | | `teams` | Gives your application access to team or channel details | `api` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): members = await ctx.api.conversations.members.get(ctx.activity.conversation.id) ``` ```python @app.on_activity("meetingStart") async def handle_meeting_start(ctx: ActivityContext): meeting_id = ctx.activity.channel_data.meeting.id tenant_id = ctx.activity.channel_data.tenant.id user_id = ctx.activity.from_.aad_object_id if meeting_id and tenant_id and user_id: participant = await ctx.api.meetings.get_participant(meeting_id, user_id, tenant_id) # participant.meeting.role — "Organizer", "Presenter", "Attendee" # participant.meeting.in_meeting — True/False ``` ```python members = await app.api.conversations.members.get("...") ``` ================================================ FILE: teams.md/src/components/include/essentials/api/typescript.incl.md ================================================ `app.api` | Area | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | | `meetings` | Gives your application access to meeting details and participant information via `getById` and `getParticipant` | | `teams` | Gives your application access to team or channel details | `api` ```typescript app.on('message', async ({ activity, api }) => { const members = await api.conversations.members(activity.conversation.id).get(); }); ``` ```typescript app.on('meetingStart', async ({ activity, api }) => { const meetingId = activity.channelData?.meeting?.id; const tenantId = activity.channelData?.tenant?.id; const userId = activity.from?.aadObjectId; if (meetingId && tenantId && userId) { const participant = await api.meetings.getParticipant(meetingId, userId, tenantId); // participant.meeting?.role — "Organizer", "Presenter", "Attendee" // participant.meeting?.inMeeting — true/false } }); ``` ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; const res = await app.api.graph.call(endpoints.chats.getAllMessages.get); ``` ================================================ FILE: teams.md/src/components/include/essentials/app-authentication/csharp.incl.md ================================================ :::note The environment file approach is not yet supported for C#. You need to configure authentication programmatically in your code. ::: In your `Program.cs`, replace the initialization: ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddTeams(); ``` with the following code to enable User Assigned Managed Identity authentication: ```csharp var builder = WebApplication.CreateBuilder(args); Func> createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = Environment.GetEnvironmentVariable("CLIENT_ID"); var managedIdentityCredential = new ManagedIdentityCredential(clientId); var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); return new TokenResponse { TokenType = "Bearer", AccessToken = accessToken.Token, }; }; var appBuilder = App.Builder() .AddCredentials(new TokenCredentials( Environment.GetEnvironmentVariable("CLIENT_ID") ?? string.Empty, async (tenantId, scopes) => { return await createTokenFactory(scopes, tenantId); } )); builder.AddTeams(appBuilder); ``` The `createTokenFactory` function provides a method to retrieve access tokens from Azure on demand, and `TokenCredentials` passes this method to the app. ## Configuration Set the following environment variable: - `CLIENT_ID`: Your Application (client) ID :::note Support for C# is coming soon. ::: ================================================ FILE: teams.md/src/components/include/essentials/app-authentication/python.incl.md ================================================ Your application should automatically use User Managed Identity authentication when you provide the `CLIENT_ID` environment variable without a `CLIENT_SECRET`. ## Configuration Set the following environment variables in your application: - `CLIENT_ID`: Your Application (client) ID - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-client-id-here # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` N/A ================================================ FILE: teams.md/src/components/include/essentials/app-authentication/typescript.incl.md ================================================ Your application should automatically use User Managed Identity authentication when you provide the `CLIENT_ID` environment variable without a `CLIENT_SECRET`. ## Configuration Set the following environment variables in your application: - `CLIENT_ID`: Your Application (client) ID - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-client-id-here # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` N/A ================================================ FILE: teams.md/src/components/include/essentials/csharp.incl.md ================================================ - Event: Anything interesting that happens on Teams — or within your application as a result of handling an earlier event. - Activity: A special type of Teams-specific event. Activities include things like messages, reactions, and adaptive card actions. - InvokeActivity: A specific kind of activity triggered by user interaction (like submitting a form), which may or may not require a response. - Handler: The logic in your application that reacts to events or activities. Handlers decide what to do, when, and how to respond. Event Handler (app.OnEvent()) Activity Handlers (app.OnActivity()) ================================================ FILE: teams.md/src/components/include/essentials/graph/csharp.incl.md ================================================ `Microsoft.Graph` package N/A N/A `app.Graph` N/A ```csharp // Equivalent of https://learn.microsoft.com/en-us/graph/api/user-get // Gets the details of the bot-user var user = app.Graph.Me.GetAsync().GetAwaiter().GetResult(); Console.WriteLine($"User ID: {user.id}"); Console.WriteLine($"User Display Name: {user.displayName}"); Console.WriteLine($"User Email: {user.mail}"); Console.WriteLine($"User Job Title: {user.jobTitle}"); ``` To access the graph using the user's token, you need to do this as part of a message handler: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```csharp app.OnMessage(async (context, cancellationToken) => { var user = await context.UserGraph.Me.GetAsync(); Console.WriteLine($"User ID: {user.id}"); Console.WriteLine($"User Display Name: {user.displayName}"); Console.WriteLine($"User Email: {user.mail}"); Console.WriteLine($"User Job Title: {user.jobTitle}"); }); ``` `userGraph` `appGraph` `app.Graph` N/A N/A ================================================ FILE: teams.md/src/components/include/essentials/graph/python.incl.md ================================================ `microsoft-teams-graph` package N/A N/A `app.graph` to call the endpoint of your choice ```python # Equivalent of https://learn.microsoft.com/en-us/graph/api/user-get # Gets the details of the bot-user user = await app.graph.me.get() print(f"User ID: {user.id}") print(f"User Display Name: {user.display_name}") print(f"User Email: {user.mail}") print(f"User Job Title: {user.job_title}") ``` You can also access the graph using the user's token from within a message handler via the `user_graph` property. ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): user = await ctx.user_graph.me.get() print(f"User ID: {user.id}") print(f"User Display Name: {user.display_name}") print(f"User Email: {user.mail}") print(f"User Job Title: {user.job_title}") ``` `user_graph` `app_graph` `app.graph` N/A N/A ================================================ FILE: teams.md/src/components/include/essentials/graph/typescript.incl.md ================================================ `@microsoft/teams.graph`, `@microsoft/teams.graph-endpoints` and `@microsoft/teams.graph-endpoints-beta` packages :::note If you're migrating from an earlier preview version of the Teams SDK, please see the [migration guide](../migrations/v2-previews) for details on breaking changes. ::: ## Package overview The Graph API surface is vast, and this is reflected in the size of the endpoints packages. To help you manage the size of your product, we made sure that the endpoints code is tree-shakable. We also made most of the code into an optional dependency, in case tree-shaking is not supported in your environment. | Package | Optional | Contains | | --------------------------------------- | -------- | ----------------------------------------------------------------------------------- | | `@microsoft/teams.graph` | No | A tiny client to create and issue Graph HTTP requests. | | `@microsoft/teams.graph-endpoints` | Yes | Request-builder functions and types to call any of the production ready Graph APIs. | | `@microsoft/teams.graph-endpoints-beta` | Yes | Same, but for Graph APIs still in preview. | To use this SDK to call Graph APIs, the first step is to install the optional endpoints package using your favorite package manager. For instance: ```sh npm install @microsoft/teams.graph-endpoints ``` `app.graph` to call the endpoint of your choice ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; // Equivalent of https://learn.microsoft.com/en-us/graph/api/user-get // Gets the details of the bot-user app.graph.call(endpoints.me.get).then((user) => { console.log(`User ID: ${user.id}`); console.log(`User Display Name: ${user.displayName}`); console.log(`User Email: ${user.mail}`); console.log(`User Job Title: ${user.jobTitle}`); }); ``` You can also access the graph using the user's token from within a message handler via the `userGraph` prop. ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; // Gets details of the current user app.on('message', async ({ activity, userGraph }) => { const me = await userGraph.call(endpoints.me.get); console.log(`User ID: ${me.id}`); console.log(`User Display Name: ${me.displayName}`); console.log(`User Email: ${me.mail}`); console.log(`User Job Title: ${me.jobTitle}`); }); ``` `userGraph` `appGraph` `app.graph` ## The Graph Client The Graph Client provides a straight-forward `call` method to interact with Microsoft Graph and issue requests scoped to a specific user or application. Paired with the Graph Endpoints packages, it offers discoverable and type-safe access to the vast Microsoft Graph API surface. Having an understanding of [how the graph API works](https://learn.microsoft.com/en-us/graph/use-the-api) will help you make the most of the SDK. For example, to get the `id` of the chat instance between a user and an app, [Microsoft Graph](https://learn.microsoft.com/en-us/graph/api/userscopeteamsappinstallation-get-chat?view=graph-rest-1.0&tabs=http) exposes it via: ``` GET /users/{user-id | user-principal-name}/teamwork/installedApps/{app-installation-id}/chat ``` The equivalent using the graph client would look like this: ```ts import { users } from '@microsoft/teams.graph-endpoints'; const chat = await userGraph.call(users.teamwork.installedApps.chat.get, { 'user-id': user.id, 'userScopeTeamsAppInstallation-id': appInstallationId, $select: ['id'], }); ``` Graph APIs often accept arguments that may go into the URL path, the query string, or the request body. As illustrated in this example, all arguments are provided as a second parameter to the `graph.call` method. The graph client puts each value in its place and attaches an authentication token as the request is constructed, and performs the fetch request for you. ## Graph Preview APIs The Graph Preview APIs are not recommended for production use. However, if you have a need to explore preview APIs, the `@microsoft/teams.graph-endpoints-beta` package makes it easy. First, install the optional dependency: ```sh npm install @microsoft/teams.graph-endpoints-beta ``` Then use it just like the regular `@microsoft/teams.graph-endpoints` package. ```ts import * as endpointsBeta from '@microsoft/teams.graph-endpoints-beta'; // Gets the current user details from /beta/me, rather than from /v1.0/me. const me = await app.graph.call(endpointsBeta.me.get); ``` The key differences between `@microsoft/teams.graph-endpoints` and `@microsoft/teams.graph-endpoints-beta` are that they represent different Graph API schemas, and that the `graph.call()` method knows to route the request to either /v1.0 or /beta. This means that it's possible to mix'n'match v1.0 and beta endpoints, for instance to explore a novel beta API in a code base that's already relying on v1.0 for all stable APIs. ## Custom Graph API calls It's possible to craft custom builder functions that work just like the ones provided in the `@microsoft/teams.graph-endpoints` and `@microsoft/teams.graph-endpoints-beta` packages. This can be handy if you wish to provide narrower return types, call some novel API that is supported by the Graph backend but not yet included in the endpoints packages, or avoid taking a dependency on the endpoints packages altogether. For instance, this will `GET https://graph.microsoft.com/beta/me?$select=displayName` and return an object typed to contain just `displayName`, without taking a dependency on the endpoints packages. ```ts import { type EndpointRequest } from '@microsoft/teams.graph'; const getMyDisplayName = (): EndpointRequest<{ displayName: string }> => ({ ver: 'beta', // use the beta endpoint; defaults to 'v1.0' if omitted method: 'get', // HTTP method to use path: '/me', // endpoint path paramDefs: { query: ['$select'], // the $select parameter goes in the query string }, params: { $select: ['displayName'], // the attribute(s) to select }, }); const { displayName } = await app.graph.call(getMyDisplayName); ``` ## Additional resources Microsoft Graph offers an extensive and thoroughly documented API surface. These essential resources will serve as your go-to references for any Graph development work: - The [Microsoft Graph Rest API reference documentation](https://learn.microsoft.com/en-us/graph/api/overview) gives details for each API, including permissions requirements. - The [Microsoft Graph REST API beta endpoint reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-beta) gives similar information for preview APIs. - The [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) lets you discover and test drive APIs. In addition, the following endpoints may be especially interesting to Teams developers: | Graph endpoints | Description | | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | | [appCatalogs](https://learn.microsoft.com/en-us/graph/api/appcatalogs-list-teamsapps?view=graph-rest-1.0) | Apps in the Teams App Catalog | | [appRoleAssignments](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list-approleassignments?view=graph-rest-1.0) | App role assignments | | [applicationTemplates](https://learn.microsoft.com/en-us/graph/api/resources/applicationtemplate?view=graph-rest-1.0) | Applications in the Microsoft Entra App Gallery | | [applications](https://learn.microsoft.com/en-us/graph/api/resources/application?view=graph-rest-1.0) | Application resources | | [chats](https://learn.microsoft.com/en-us/graph/api/chat-list?view=graph-rest-1.0&tabs=http) | Chat resources between users | | [communications](https://learn.microsoft.com/en-us/graph/api/application-post-calls?view=graph-rest-1.0) | Calls and Online meetings | | [employeeExperience](https://learn.microsoft.com/en-us/graph/api/resources/engagement-api-overview?view=graph-rest-1.0) | Employee Experience and Engagement | | [me](https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http) | Same as `/users` but scoped to one user (who is making the request) | | [teams](https://learn.microsoft.com/en-us/graph/api/resources/team?view=graph-rest-1.0) | Team resources in Microsoft Teams | | [teamsTemplates](https://learn.microsoft.com/en-us/microsoftteams/get-started-with-teams-templates) | Templates used to create teams | | [teamwork](https://learn.microsoft.com/en-us/graph/api/resources/teamwork?view=graph-rest-1.0) | A range of Microsoft Teams functionalities | | [users](https://learn.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0) | User resources | ================================================ FILE: teams.md/src/components/include/essentials/on-activity/csharp.incl.md ================================================ The Teams SDK exposes a fluent router so you can subscribe to these activities with `app.OnActivity(...)` using minimal APIs. ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send($"you said: {context.activity.Text}", cancellationToken); }); ``` In the above example, the `context.activity` parameter is of type `MessageActivity`, which has a `Text` property. You'll notice that the handler here does not return anything, but instead handles it by `send`ing a message back. For message activities, Teams does not expect your application to return anything (though it's usually a good idea to send some sort of friendly acknowledgment!). The `OnActivity` activity handlers (and attributes) follow a [middleware](https://www.patterns.dev/vanilla/mediator-pattern/) pattern similar to how `dotnet` middlewares work. This means that for each activity handler, a `Next` function is passed in which can be called to pass control to the next handler. This allows you to build a chain of handlers that can process the same activity in different ways. ```csharp app.OnMessage(async (context, cancellationToken) => { Console.WriteLine("global logger"); context.Next(); // pass control onward return Task.CompletedTask; }); ``` ```csharp app.OnMessage(async (context, cancellationToken) => { if (context.Activity.Text == "/help") { await context.Send("Here are all the ways I can help you...", cancellationToken); } // Conditionally pass control to the next handler context.Next(); }); app.OnMessage(async (context, cancellationToken) => { // Fallthrough to the final handler await context.Send($"Hello! you said {context.Activity.Text}", cancellationToken); }); ``` N/A ================================================ FILE: teams.md/src/components/include/essentials/on-activity/python.incl.md ================================================ The Teams SDK exposes a fluent router so you can subscribe to these activities with `@app.event("activity")`. ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.send(f"You said '{ctx.activity.text}'") ``` In the above example, the `ctx.activity` parameter is of type `MessageActivity`, which has a `text` property. You'll notice that the handler here does not return anything, but instead handles it by `send`ing a message back. For message activities, Teams does not expect your application to return anything (though it's usually a good idea to send some sort of friendly acknowledgment!). The `event` activity handlers (and attributes) follow a [middleware](https://www.patterns.dev/vanilla/mediator-pattern/) pattern similar to how `python` middlewares work. This means that for each activity handler, a `next` function is passed in which can be called to pass control to the next handler. This allows you to build a chain of handlers that can process the same activity in different ways. ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle message activities using the new generated handler system.""" print(f"[GENERATED onMessage] Message received: {ctx.activity.text}") await ctx.next() ``` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle message activities using the new generated handler system.""" if ctx.activity.text == "/help": await ctx.send("Here are all the ways I can help you...") await ctx.next() ``` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.send(f"You said '{ctx.activity.text}'") ``` N/A ================================================ FILE: teams.md/src/components/include/essentials/on-activity/typescript.incl.md ================================================ The Teams SDK exposes a fluent router so you can subscribe to these activities with `app.on('', …)`. ```typescript app.on('message', async ({ activity, send }) => { await send(`You said: ${activity.text}`); }); ``` In the above example, the `activity` parameter is of type `MessageActivity`, which has a `text` property. You'll notice that the handler here does not return anything, but instead handles it by `send`ing a message back. For message activities, Teams does not expect your application to return anything (though it's usually a good idea to send some sort of friendly acknowledgment!). [Other activity types](./activity-ref) have different properties and different required results. For a given handler, the SDK will automatically determine the type of `activity` and also enforce the correct return type. The `on` activity handlers follow a [middleware](https://www.patterns.dev/vanilla/mediator-pattern/) pattern similar to how `express` middlewares work. This means that for each activity handler, a `next` function is passed in which can be called to pass control to the next handler. This allows you to build a chain of handlers that can process the same activity in different ways. ```typescript app.on('message', async ({ next }) => { console.log('global logger'); next(); // pass control onward }); ``` ```typescript app.on('message', async ({ activity, next }) => { if (activity.text === '/help') { await send('Here are all the ways I can help you...'); return; } // Conditionally pass control to the next handler next(); }); ``` ```typescript app.on('message', async ({ activity }) => { // Fallthrough to the final handler await send(`Hello! you said ${activity.text}`); }); ``` ## Activity Reference For a list of supported activities that your application can listen to, see the [activity reference](./activity-ref). ================================================ FILE: teams.md/src/components/include/essentials/on-event/csharp.incl.md ================================================ ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting AppEventHandlers["Event Handler (app.OnEvent())"]:::interesting Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers linkStyle 0,1,2,3,4 stroke:#b1650f,stroke-width:1px classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; ``` | **Event Name** | **Description** | | ------------------- | ------------------------------------------------------------------------------ | | `start` | Triggered when your application starts. Useful for setup or boot-time logging. | | `signin` | Triggered during a sign-in flow via Teams. | | `error` | Triggered when an unhandled error occurs in your app. Great for diagnostics. | | `activity` | A catch-all for incoming Teams activities (messages, commands, etc.). | | `activity.response` | Triggered when your app sends a response to an activity. Useful for logging. | | `activity.sent` | Triggered when an activity is sent (not necessarily in response). | ```csharp app.OnError((sender, @event) => { // do something with the error app.Logger.Info(@event.Exception.ToString()); }); ``` When an activity is received, log its `JSON` payload. ```csharp app.OnActivity((sender, @event) => { app.Logger.Info(@event.Activity.ToString()); }); ``` ================================================ FILE: teams.md/src/components/include/essentials/on-event/python.incl.md ================================================ ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting AppEventHandlers["Event Handler (app.event())"]:::interesting Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers linkStyle 0,1,2,3,4 stroke:#b1650f,stroke-width:1px classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; ``` | **Event Name** | **Description** | | ------------------- | ------------------------------------------------------------------------------ | | `start` | Triggered when your application starts. Useful for setup or boot-time logging. | | `sign_in` | Triggered during a sign-in flow via Teams. | | `error` | Triggered when an unhandled error occurs in your app. Great for diagnostics. | | `activity` | Triggered for all incoming Teams activities (messages, commands, etc.). | | `activity_response` | Triggered when your app sends a response to an activity. Useful for logging. | | `activity_sent` | Triggered when an activity is sent (not necessarily in response). |
:::info Event handler registration uses `@app.event("")` with an async function that receives an event object specific to the event type (e.g., `ErrorEvent`, `ActivityEvent`). ::: ```python @app.event("error") async def handle_error(event: ErrorEvent): """Handle error events.""" print(f"Error occurred: {event.error}") if hasattr(event, "context") and event.context: print(f"Context: {event.context}") ``` When an activity is received, log its payload. ```python @app.event("activity") async def handle_activity(event: ActivityEvent): """Handle activity events.""" print(f"Activity received: {event.activity}") ``` ================================================ FILE: teams.md/src/components/include/essentials/on-event/typescript.incl.md ================================================ ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting AppEventHandlers["Event Handler (app.event())"]:::interesting Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers linkStyle 0,1,2,3,4 stroke:#b1650f,stroke-width:1px classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; ``` | **Event Name** | **Description** | | ------------------- | ------------------------------------------------------------------------------ | | `start` | Triggered when your application starts. Useful for setup or boot-time logging. | | `signin` | Triggered during a sign-in flow via Teams. | | `error` | Triggered when an unhandled error occurs in your app. Great for diagnostics. | | `activity` | A catch-all for incoming Teams activities (messages, commands, etc.). | | `activity.response` | Triggered when your app sends a response to an activity. Useful for logging. | | `activity.sent` | Triggered when an activity is sent (not necessarily in response). | ```typescript app.event('error', ({ error }) => { app.log.error(error); // Or Alternatively, send it to an observability platform }); ``` When a user signs in using `OAuth` or `SSO`, use the graph api to fetch their profile and say hello. ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; app.event('signin', async ({ activity, send, userGraph }) => { const me = await userGraph.call(endpoints.me.get); await send(`👋 Hello ${me.name}`); }); ``` ================================================ FILE: teams.md/src/components/include/essentials/python.incl.md ================================================ - Event: Anything interesting that happens on Teams — or within your application as a result of handling an earlier event. - Activity: A special type of Teams-specific event. Activities include things like messages, reactions, and adaptive card actions. - InvokeActivity: A specific kind of activity triggered by user interaction (like submitting a form), which may or may not require a response. - Handler: The logic in your application that reacts to events or activities. Handlers decide what to do, when, and how to respond. Event Handler decorator (@app.event()) Activity Handler decorators (@app.on_activity()) ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/csharp.incl.md ================================================ ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send($"you said: {context.activity.Text}", cancellationToken); }); ``` ```csharp app.OnVerifyState(async (context, cancellationToken) => { await context.Send("You have successfully signed in!", cancellationToken); }); ``` `SignIn.VerifyState` ```csharp app.OnMessage(async (context, cancellationToken) => { context.Stream.Emit("hello"); context.Stream.Emit(", "); context.Stream.Emit("world!"); // result message: "hello, world!" return Task.CompletedTask; }); ``` `AddMention` ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send(new MessageActivity("hi!").AddMention(activity.From), cancellationToken); }); ``` `WithRecipient` ```csharp app.OnMessage(async (context, cancellationToken) => { // Using WithRecipient with isTargeted=true explicitly targets the specified recipient await context.Send( new MessageActivity("This message is only visible to you!") .WithRecipient(context.Activity.From, isTargeted: true), cancellationToken ); }); ``` :::tip[.NET] In .NET, targeted message APIs are marked with `[Experimental("ExperimentalTeamsTargeted")]` and will produce a compiler error until you opt in. Suppress the diagnostic inline with `#pragma warning disable ExperimentalTeamsTargeted` or project-wide in your `.csproj`: ```xml $(NoWarn);ExperimentalTeamsTargeted ``` ::: :::tip[.NET] In .NET, reaction APIs are marked with `[Experimental("ExperimentalTeamsReactions")]` and will produce a compiler error until you opt in. Suppress the diagnostic inline with `#pragma warning disable ExperimentalTeamsReactions` or project-wide in your `.csproj`: ```xml $(NoWarn);ExperimentalTeamsReactions ``` ::: ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/proactive-messaging/csharp.incl.md ================================================ `conversationId` import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```csharp app.OnInstall(async (context, cancellationToken) => { // Save the conversation id in context.Storage.Set(activity.From.AadObjectId!, activity.Conversation.Id); await context.Send("Hi! I am going to remind you to say something to me soon!", cancellationToken); notificationQueue.AddReminder(activity.From.AadObjectId!, Notifications.SendProactive, 10_000); }); ``` ```csharp public static class Notifications { public static async Task SendProactive(string userId) { var conversationId = (string?)storage.Get(userId); if (conversationId is null) return; await app.Send(conversationId, "Hey! It's been a while. How are you?"); } } ``` ```csharp // When sending proactively, you must provide an explicit recipient account public static async Task SendTargetedNotification(string conversationId, Account recipient) { var teams = app.UseTeams(); await teams.Send( conversationId, new MessageActivity("This is a private notification just for you!") .WithRecipient(recipient, isTargeted: true) ); } ``` ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/proactive-messaging/python.incl.md ================================================ `conversation_id` ```python from microsoft_teams.api import InstalledActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext # ... # This would be some persistent storage storage = dict[str, str]() # Installation is just one place to get the conversation_id. All activities have this field as well. @app.on_install_add async def handle_install_add(ctx: ActivityContext[InstalledActivity]): # Save the conversation_id storage[ctx.activity.from_.aad_object_id] = ctx.activity.conversation.id await ctx.send("Hi! I am going to remind you to say something to me soon!") # This queues up the proactive notifaction to be sent in 1 minute notication_queue.add_reminder(ctx.activity.from_.aad_object_id, send_proactive_notification, 60000) ``` ```python from microsoft_teams.api import MessageActivityInput # ... async def send_proactive_notification(user_id: str): conversation_id = storage.get(user_id, "") if not conversation_id: return activity = MessageActivityInput(text="Hey! It's been a while. How are you?") await app.send(conversation_id, activity) ``` ```python from microsoft_teams.api import MessageActivityInput, Account # When sending proactively, you must provide an explicit recipient account async def send_targeted_notification(conversation_id: str, recipient: Account): await app.send( conversation_id, MessageActivityInput(text="This is a private notification just for you!") .with_recipient(recipient, is_targeted=True) ) ``` ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/proactive-messaging/typescript.incl.md ================================================ `conversationId` ```typescript import { MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... // This would be some persistent storage const myConversationIdStorage = new Map(); // Installation is just one place to get the conversation id. All activities // have the conversation id, so you can use any activity to get it. app.on('install.add', async ({ activity, send }) => { // Save the conversation id in myConversationIdStorage.set(activity.from.aadObjectId!, activity.conversation.id); await send('Hi! I am going to remind you to say something to me soon!'); notificationQueue.addReminder(activity.from.aadObjectId!, sendProactiveNotification, 10_000); }); ``` ```typescript import { MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... const sendProactiveNotification = async (userId: string) => { const conversationId = myConversationIdStorage.get(userId); if (!conversationId) { return; } const activity = new MessageActivity('Hey! It\'s been a while. How are you?'); await app.send(conversationId, activity); }; ``` ```typescript import { MessageActivity, Account } from '@microsoft/teams.api'; // When sending proactively, you must provide an explicit recipient account const sendTargetedNotification = async (conversationId: string, recipient: Account) => { await app.send( conversationId, new MessageActivity('This is a private notification just for you!') .withRecipient(recipient, true) ); }; ``` ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/python.incl.md ================================================ ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.send(f"You said '{ctx.activity.text}'") ``` ```python @app.event("sign_in") async def handle_sign_in(event: SignInEvent): """Handle sign-in events.""" await event.activity_ctx.send("You are now signed in!") ``` `sign_in` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): ctx.stream.update("Stream starting...") await asyncio.sleep(1) # Stream messages with delays using ctx.stream.emit for message in STREAM_MESSAGES: # Add some randomness to timing await asyncio.sleep(random()) ctx.stream.emit(message) ``` `add_mention` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.send(MessageActivityInput(text='hi!').add_mention(account=ctx.activity.from_)) ``` `with_recipient` ```python from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): # Using with_recipient with is_targeted=True explicitly targets the specified recipient await ctx.send( MessageActivityInput(text="This message is only visible to you!") .with_recipient(ctx.activity.from_, is_targeted=True) ) ``` N/A N/A ================================================ FILE: teams.md/src/components/include/essentials/sending-messages/typescript.incl.md ================================================ ```typescript app.on('message', async ({ activity, send }) => { await send(`You said: ${activity.text}`); }); ``` ```typescript app.on('signin.verify-state', async ({ send }) => { await send('You have successfully signed in!'); }); ``` `signin.verify-state` ```typescript app.on('message', async ({ activity, stream }) => { stream.emit('hello'); stream.emit(', '); stream.emit('world!'); // result message: "hello, world!" }); ``` `addMention` ```typescript app.on('message', async ({ send, activity }) => { await send(new MessageActivity('hi!').addMention(activity.from)); }); ``` `withRecipient` ```typescript import { MessageActivity } from '@microsoft/teams.api'; app.on('message', async ({ send, activity }) => { // Using withRecipient with isTargeted=true explicitly targets the specified recipient await send( new MessageActivity('This message is only visible to you!') .withRecipient(activity.from, true) ); }); ``` N/A N/A ================================================ FILE: teams.md/src/components/include/essentials/typescript.incl.md ================================================ - Event: Anything interesting that happens on Teams — or within your application as a result of handling an earlier event. - Activity: A special type of Teams-specific event. Activities include things like messages, reactions, and adaptive card actions. - InvokeActivity: A specific kind of activity triggered by user interaction (like submitting a form), which may or may not require a response. - Handler: The logic in your application that reacts to events or activities. Handlers decide what to do, when, and how to respond. Event Handler (app.event()) Activity Handlers (app.on()) ================================================ FILE: teams.md/src/components/include/getting-started/_LLMs/csharp.incl.md ================================================ C# **Small**: [llms_csharp.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_csharp.txt) - This file contains an index of the various pages in the C# documentation. The agent needs to selectively read the relevant pages to answer questions and help with development. **Large**: [llms_csharp_full.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_csharp_full.txt) - This file contains the full content of the C# documentation, including all pages and code snippets. The agent can keep the entire documentation in memory to answer questions and help with development. ================================================ FILE: teams.md/src/components/include/getting-started/_LLMs/python.incl.md ================================================ Python **Small**: [llms_python.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_python.txt) - This file contains an index of the various pages in the Python documentation. The agent needs to selectively read the relevant pages to answer questions and help with development. **Large**: [llms_python_full.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_python_full.txt) - This file contains the full content of the Python documentation, including all pages and code snippets. The agent can keep the entire documentation in memory to answer questions and help with development. ================================================ FILE: teams.md/src/components/include/getting-started/_LLMs/typescript.incl.md ================================================ TypeScript **Small**: [llms_typescript.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_typescript.txt) - This file contains an index of the various pages in the TypeScript documentation. The agent needs to selectively read the relevant pages to answer questions and help with development. **Large**: [llms_typescript_full.txt](https://microsoft.github.io/teams-sdk/llms_docs/llms_typescript_full.txt) - This file contains the full content of the TypeScript documentation, including all pages and code snippets. The agent can keep the entire documentation in memory to answer questions and help with development. ================================================ FILE: teams.md/src/components/include/getting-started/code-basics/csharp.incl.md ================================================ N/A ``` Quote.Agent/ |── appPackage/ # Teams app package files ├── Program.cs # Main application startup code ``` - **appPackage/**: Contains the Teams app package files, including the `manifest.json` file and icons. This is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams for testing. The app manifest defines the app's metadata, capabilities, and permissions. ```csharp title="Program.cs" using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); builder.AddTeams().AddTeamsDevTools(); var app = builder.Build(); var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { await context.Typing(cancellationToken); await context.Send($"you said '{context.Activity.Text}'", cancellationToken); }); app.Run(); ``` (onActivity, onActivitySent, etc.) ```csharp title="Program.cs" teams.OnMessage(async (context, cancellationToken) => { await context.Typing(cancellationToken); await context.Send($"you said \"{context.activity.Text}\"", cancellationToken); }); ``` Listens for all incoming messages using `onMessage` handler. Responds by echoing back the received message. :::info Each activity type has both an attribute and a functional method for type safety/simplicity of routing logic! ::: ```csharp var app = builder.Build(); app.UseTeams(); app.Run(); ``` ================================================ FILE: teams.md/src/components/include/getting-started/code-basics/python.incl.md ================================================ N/A ``` quote-agent/ |── appPackage/ # Teams app package files ├── src ├── main.py # Main application code ``` - **appPackage/**: Contains the Teams app package files, including the `manifest.json` file and icons. This is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams for testing. The app manifest defines the app's metadata, capabilities, and permissions. - **src/**: Contains the main application code. The `main.py` file is the entry point for your application. ```python title="src/main.py" from microsoft_teams.api import MessageActivity, TypingActivityInput from microsoft_teams.apps import ActivityContext, App, AppOptions from microsoft_teams.devtools import DevToolsPlugin app = App(plugins=[DevToolsPlugin()]) ``` (on_activity, on_activity_sent, etc.) ```python title="src/main.py" @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(TypingActivityInput()) if "reply" in ctx.activity.text.lower(): await ctx.reply("Hello! How can I assist you today?") else: await ctx.send(f"You said '{ctx.activity.text}'") ``` Listens for all incoming messages using `app.on_message` Responds by echoing back the received message if any other text aside from "reply" is sent. :::info Python uses type hints for better development experience. You can change the activity handler to different supported activities, and the type system will provide appropriate hints and validation. ::: ```python if __name__ == "__main__": asyncio.run(app.start()) ``` ================================================ FILE: teams.md/src/components/include/getting-started/code-basics/typescript.incl.md ================================================ N/A ``` quote-agent/ |── appPackage/ # Teams app package files ├── src/ │ └── index.ts # Main application code ``` - **appPackage/**: Contains the Teams app package files, including the `manifest.json` file and icons. This is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams for testing. The app manifest defines the app's metadata, capabilities, and permissions. - **src/**: Contains the main application code. The `index.ts` file is the entry point for your application. ```typescript title="src/index.ts" import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common/logging'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; const app = new App({ plugins: [new DevtoolsPlugin()], }); ``` (onActivity, onActivitySent, etc.) ```typescript title="src/index.ts" app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); await send(`you said "${activity.text}"`); }); ``` Listens for all incoming messages using `app.on('message')`. Responds by echoing back the received message. :::info Type safety is a core tenet of this version of the SDK. You can change the activity `name` to a different supported value, and the type system will automatically adjust the type of activity to match the new value. ::: ```typescript title="src/index.ts" await app.start(); ``` ================================================ FILE: teams.md/src/components/include/getting-started/csharp.incl.md ================================================ N/A C# ================================================ FILE: teams.md/src/components/include/getting-started/python.incl.md ================================================ :::warning Our Python SDK is currently in Public Preview. We're going to do our best to not ship breaking changes, but breaking changes may happen from time to time ::: Python ================================================ FILE: teams.md/src/components/include/getting-started/quickstart/csharp.incl.md ================================================ - **.NET** v.8 or higher. Install or upgrade from [dotnet.microsoft.com](https://dotnet.microsoft.com/en-us/download). ```sh npx @microsoft/teams.cli@latest new csharp quote-agent --template echo ``` 1. Creates a new directory called `Quote.Agent`. 2. Bootstraps the echo agent template files into your project directory. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `Quote.Agent/appPackage` directory. The Teams [app manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams. 1. Navigate to your new agent's directory: ```sh cd Quote.Agent/Quote.Agent ``` 2. Install the dependencies: ```sh dotnet restore ``` 3. Start the development server: ```sh dotnet run ``` 4. In the console, you should see a similar output: ```sh [INFO] Microsoft.Hosting.Lifetime Now listening on: http://localhost:3978 [WARN] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools ⚠️ Devtools are not secure and should not be used production environments ⚠️ [INFO] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools Available at http://localhost:3979/devtools [INFO] Microsoft.Hosting.Lifetime Application started. Press Ctrl+C to shut down. [INFO] Microsoft.Hosting.Lifetime Hosting environment: Development ``` N/A N/A N/A ================================================ FILE: teams.md/src/components/include/getting-started/quickstart/python.incl.md ================================================ - **Python** v3.12 or higher. Install or upgrade from [python.org/downloads](https://www.python.org/downloads/). ```sh npx @microsoft/teams.cli@latest new python quote-agent --template echo ``` 1. Creates a new directory called `quote-agent`. 2. Bootstraps the echo agent template files into it under `quote-agent/src`. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `quote-agent/appPackage` directory. The Teams [app manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams. Navigate to your new agent's directory: ```sh cd quote-agent ``` Start the development server: ```sh python src/main.py ``` In the console, you should see a similar output: ```sh [INFO] @teams/app Successfully initialized all plugins [WARNING] @teams/app.DevToolsPlugin ⚠️ Devtools is not secure and should not be used in production environments ⚠️ [INFO] @teams/app.HttpPlugin Starting HTTP server on port 3978 INFO: Started server process [6436] INFO: Waiting for application startup. [INFO] @teams/app.DevToolsPlugin available at http://localhost:3979/devtools [INFO] @teams/app.HttpPlugin listening on port 3978 🚀 [INFO] @teams/app Teams app started successfully [INFO] @teams/app.DevToolsPlugin listening on port 3979 🚀 INFO: Application startup complete.. INFO: Uvicorn running on http://0.0.0.0:3979 (Press CTRL+C to quit) ``` ```sh pip install microsoft-teams-apps ``` ```python import asyncio import uvicorn from fastapi import FastAPI # highlight-next-line from microsoft_teams.apps import App, FastAPIAdapter # Your existing FastAPI app my_fastapi = FastAPI() # highlight-start # Wrap your app in an adapter and create the Teams app adapter = FastAPIAdapter(app=my_fastapi) app = App(http_server_adapter=adapter) @app.on_message async def handle_message(ctx): await ctx.send(f"You said: {ctx.activity.text}") # highlight-end async def main(): # highlight-next-line await app.initialize() # Register the Teams endpoint (does not start a server) # Start your server as usual config = uvicorn.Config(app=my_fastapi, host="0.0.0.0", port=3978) server = uvicorn.Server(config) await server.serve() asyncio.run(main()) ``` See the [HTTP Server guide](../in-depth-guides/server/http-server) for full details on adapters and custom server setups. ================================================ FILE: teams.md/src/components/include/getting-started/quickstart/typescript.incl.md ================================================ - **Node.js** v.20 or higher. Install or upgrade from [nodejs.org](https://nodejs.org/). ```sh npx @microsoft/teams.cli@latest new typescript quote-agent --template echo ``` 1. Creates a new directory called `quote-agent`. 2. Bootstraps the echo agent template files into it under `quote-agent/src`. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `quote-agent/appPackage` directory. The Teams [app manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams. 1. Navigate to your new agent's directory: ```sh cd quote-agent ``` 2. Install the dependencies: ```sh npm install ``` 3. Start the development server: ```sh npm run dev ``` 4. In the console, you should see a similar output: ```sh > quote-agent@0.0.0 dev > npx nodemon -w "./src/**" -e ts --exec "node -r ts-node/register -r dotenv/config ./src/index.ts" [nodemon] 3.1.9 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): src/** [nodemon] watching extensions: ts [nodemon] starting `node -r ts-node/register -r dotenv/config ./src/index.ts` [WARN] @teams/app/devtools ⚠️ Devtools are not secure and should not be used production environments ⚠️ [INFO] @teams/app/http listening on port 3978 🚀 [INFO] @teams/app/devtools available at http://localhost:3979/devtools ``` ```sh npm i @microsoft/teams.apps ``` ```typescript import http from 'http'; import express from 'express'; // highlight-next-line import { App, ExpressAdapter } from '@microsoft/teams.apps'; // Your existing Express server const expressApp = express(); const server = http.createServer(expressApp); // highlight-start // Wrap your server in an adapter and create the Teams app const adapter = new ExpressAdapter(server); const app = new App({ httpServerAdapter: adapter }); app.on('message', async ({ send, activity }) => { await send(`You said: ${activity.text}`); }); // Register the Teams endpoint on your server (does not start it) await app.initialize(); // highlight-end // Start your server as usual server.listen(3978); ``` See the [HTTP Server guide](../in-depth-guides/server/http-server) for full details on adapters and custom server setups. ================================================ FILE: teams.md/src/components/include/getting-started/running-in-teams/csharp.incl.md ================================================ ```sh [INFO] Microsoft.Hosting.Lifetime Now listening on: http://localhost:3978 [WARN] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools ⚠️ Devtools are not secure and should not be used production environments ⚠️ [INFO] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools Available at http://localhost:3979/devtools [INFO] Microsoft.Hosting.Lifetime Application started. Press Ctrl+C to shut down. [INFO] Microsoft.Hosting.Lifetime Hosting environment: Development ``` ================================================ FILE: teams.md/src/components/include/getting-started/running-in-teams/python.incl.md ================================================ ```sh [INFO] @teams/app Successfully initialized all plugins [INFO] @teams/app.HttpPlugin Starting HTTP server on port 3978 INFO: Started server process [6436] INFO: Waiting for application startup. [INFO] @teams/app.HttpPlugin listening on port 3978 🚀 [INFO] @teams/app Teams app started successfully INFO: Application startup complete.. INFO: Uvicorn running on http://0.0.0.0:3979 (Press CTRL+C to quit) ``` ================================================ FILE: teams.md/src/components/include/getting-started/running-in-teams/typescript.incl.md ================================================ ```sh [nodemon] 3.1.9 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): src/** [nodemon] watching extensions: ts [nodemon] starting `node -r ts-node/register -r dotenv/config ./src/index.ts` [WARN] @teams/app/devtools ⚠️ Devtools are not secure and should not be used production environments ⚠️ [INFO] @teams/app/http listening on port 3978 🚀 [INFO] @teams/app/devtools available at http://localhost:3979/devtools ``` ================================================ FILE: teams.md/src/components/include/getting-started/typescript.incl.md ================================================ N/A TypeScript ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/building-adaptive-cards/csharp.incl.md ================================================ `Microsoft.Teams.Cards` With `Microsoft.Teams.Cards` you can build these cards entirely in C# while enjoying full IntelliSense and compiler safety. `Microsoft.Teams.Cards` exposes small **builder helpers** including `AdaptiveCard`, `TextBlock`, `ToggleInput`, `ExecuteAction`, _etc._ C# ```csharp using Microsoft.Teams.Cards; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Hello world") { Wrap = true, Weight = TextWeight.Bolder }, new ToggleInput("Notify me") { Id = "notify" } }, Actions = new List { new ExecuteAction { Title = "Submit", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_basic" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; ``` :::info The builder helpers use strongly-typed interfaces. Use IntelliSense (Ctrl+Space) or "Go to Definition" (F12) in your IDE to explore available types and properties. Source code lives in the `Microsoft.Teams.Cards` namespace. ::: ```csharp // "Huge" is not a valid size for TextBlock - this will cause a compilation error var textBlock = new TextBlock("Test") { Wrap = true, Weight = TextWeight.Bolder, Size = "Huge" // This is invalid - should be TextSize enum }; ``` ```csharp var cardJson = """ { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "verticalContentAlignment": "center", "items": [ { "type": "Image", "style": "Person", "url": "https://aka.ms/AAp9xo4", "size": "Small", "altText": "Portrait of David Claux" } ], "width": "auto" }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [ { "type": "TextBlock", "weight": "Bolder", "text": "David Claux", "wrap": true } ], "width": "auto" }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [ { "type": "TextBlock", "text": "Principal Platform Architect at Microsoft", "isSubtle": true, "wrap": true } ], "width": "stretch" } ] } ], "version": "1.5", "schema": "http://adaptivecards.io/schemas/adaptive-card.json" } """; // Deserialize the JSON into an AdaptiveCard object var card = AdaptiveCard.Deserialize(cardJson); // Send the card await client.Send(card); ``` `AdaptiveCard` ```csharp teams.OnMessage(async (context, cancellationToken) => { var text = context.Activity.Text?.ToLowerInvariant() ?? ""; if (text.Contains("form")) { await context.Typing(cancellationToken); var card = CreateTaskFormCard(); await context.Send(card, cancellationToken); } }); ``` The definition for `CreateTaskFormCard` is as follows ```csharp private static AdaptiveCard CreateTaskFormCard() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Create New Task") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextInput { Id = "title", Label = "Task Title", Placeholder = "Enter task title" }, new TextInput { Id = "description", Label = "Description", Placeholder = "Enter task details", IsMultiline = true }, new ChoiceSetInput { Id = "priority", Label = "Priority", Value = "medium", Choices = new List { new() { Title = "High", Value = "high" }, new() { Title = "Medium", Value = "medium" }, new() { Title = "Low", Value = "low" } } }, new DateInput { Id = "due_date", Label = "Due Date", Value = DateTime.Now.ToString("yyyy-MM-dd") } }, Actions = new List { new ExecuteAction { Title = "Create Task", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "create_task" } } }), AssociatedInputs = AssociatedInputs.Auto, Style = ActionStyle.Positive } } }; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/building-adaptive-cards/python.incl.md ================================================ `microsoft-teams-cards` With `microsoft-teams-cards` you can build these cards entirely in Python while enjoying full IntelliSense and compiler safety. `microsoft-teams-cards` exposes small **builder helpers** including `Card`, `TextBlock`, `ToggleInput`, `ExecuteAction`, _etc._ Python ```python from microsoft_teams.cards import AdaptiveCard, TextBlock, ToggleInput, ActionSet, ExecuteAction card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextBlock(text="Hello world", wrap=True, weight="Bolder"), ToggleInput(label="Notify me").with_id("notify"), ActionSet( actions=[ ExecuteAction(title="Submit") .with_data({"action": "submit_basic"}) .with_associated_inputs("auto") ] ), ], ) ``` :::info The builder helpers use typed dictionaries and type hints. Use your IDE's IntelliSense features to explore available properties. Source code lives in the `teams.cards` module. ::: ```python # "huge" is not a valid size for TextBlock text_block = TextBlock(text="Test", wrap=True, weight="Bolder", size="huge"), ``` ```python card = AdaptiveCard.model_validate( { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "verticalContentAlignment": "center", "items": [ { "type": "Image", "style": "Person", "url": "https://aka.ms/AAp9xo4", "size": "Small", "altText": "Portrait of David Claux", } ], "width": "auto", }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [{"type": "TextBlock", "weight": "Bolder", "text": "David Claux", "wrap": True}], "width": "auto", }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [ { "type": "TextBlock", "text": "Principal Platform Architect at Microsoft", "isSubtle": True, "wrap": True, } ], "width": "stretch", }, ], } ], "version": "1.5", } ) # Send the card as an attachment message = MessageActivityInput(text="Hello text!").add_card(card) ``` `AdaptiveCard` Notice how the builder pattern keeps the file readable and maintainable: ```python from datetime import datetime from microsoft_teams.api import MessageActivity, TypingActivityInput from microsoft_teams.apps import ActivityContext from microsoft_teams.cards import AdaptiveCard, TextBlock, ActionSet, ExecuteAction, Choice, ChoiceSetInput, DateInput, TextInput # ... @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(TypingActivityInput()) card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextBlock(text="Create New Task", weight="Bolder", size="Large"), TextInput(id="title").with_label("Task Title").with_placeholder("Enter task title"), TextInput(id="description").with_label("Description").with_placeholder("Enter task details").with_is_multiline(True), ChoiceSetInput(choices=[ Choice(title="High", value="high"), Choice(title="Medium", value="medium"), Choice(title="Low", value="low"), ]).with_id("priority").with_label("Priority").with_value("medium"), DateInput(id="due_date").with_label("Due Date").with_value(datetime.now().strftime("%Y-%m-%d")), ActionSet( actions=[ ExecuteAction(title="Create Task") .with_data({"action": "create_task"}) .with_associated_inputs("auto") .with_style("positive") ] ), ], ) await ctx.send(card) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/building-adaptive-cards/typescript.incl.md ================================================ `@microsoft/teams.cards` With `@microsoft/teams.cards` you can build these cards entirely in TypeScript/JavaScript while enjoying full IntelliSense and compiler safety. `@microsoft/teams.cards` exposes small **builder helpers** including `Card`, `TextBlock`, `ToggleInput`, `ExecuteAction`, _etc._ TypeScript/JavaScript ```ts import { AdaptiveCard, TextBlock, ToggleInput, ExecuteAction, ActionSet, } from '@microsoft/teams.cards'; const card = new AdaptiveCard( new TextBlock('Hello world', { wrap: true, weight: 'Bolder' }), new ToggleInput('Notify me').withId('notify'), new ActionSet( new ExecuteAction({ title: 'Submit' }) .withData({ action: 'submit_basic' }) .withAssociatedInputs('auto') ) ); ``` :::info Source code lives in `teams.ts/packages/cards/src/`. Feel free to inspect or extend the helpers for your own needs. ::: ```typescript // @ts-expect-error: "huge" is not a valid size for TextBlock const textBlock = new TextBlock('Valid', { size: 'huge' }); ``` ```typescript const cardJson = /* copied JSON */; const card = new AdaptiveCard().withBody(cardJson); ``` ```ts const rawCard: IAdaptiveCard = { type: 'AdaptiveCard', body: [ { text: 'Please fill out the below form to send a game purchase request.', wrap: true, type: 'TextBlock', style: 'heading', }, { columns: [ { width: 'stretch', items: [ { choices: [ { title: 'Call of Duty', value: 'call_of_duty' }, { title: "Death's Door", value: 'deaths_door' }, { title: 'Grand Theft Auto V', value: 'grand_theft' }, { title: 'Minecraft', value: 'minecraft' }, ], style: 'filtered', placeholder: 'Search for a game', id: 'choiceGameSingle', type: 'Input.ChoiceSet', label: 'Game:', }, ], type: 'Column', }, ], type: 'ColumnSet', }, ], actions: [ { title: 'Request purchase', type: 'Action.Execute', data: { action: 'purchase_item' }, }, ], version: '1.5', }; ``` `IAdaptiveCard` Notice how the builder pattern keeps the file readable and maintainable: ```ts import { AdaptiveCard, TextBlock, TextInput, ChoiceSetInput, DateInput, ActionSet, ExecuteAction, } from '@microsoft/teams.cards'; import { App } from '@microsoft/teams.apps'; // ... app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); const card = new AdaptiveCard( new TextBlock('Create New Task', { size: 'Large', weight: 'Bolder', }), new TextInput({ id: 'title' }).withLabel('Task Title').withPlaceholder('Enter task title'), new TextInput({ id: 'description' }) .withLabel('Description') .withPlaceholder('Enter task details') .withIsMultiline(true), new ChoiceSetInput( { title: 'High', value: 'high' }, { title: 'Medium', value: 'medium' }, { title: 'Low', value: 'low' } ) .withId('priority') .withLabel('Priority') .withValue('medium'), new DateInput({ id: 'due_date' }) .withLabel('Due Date') .withValue(new Date().toISOString().split('T')[0]), new ActionSet( new ExecuteAction({ title: 'Create Task' }) .withData({ action: 'create_task' }) .withAssociatedInputs('auto') .withStyle('positive') ) ); await send(card); // Or build a complex activity out that includes the card: // const message = new MessageActivity('Enter this form').addCard('adaptive', card); // await send(message); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/csharp.incl.md ================================================ Overview of Adaptive Cards in C# Teams SDK for building rich, interactive user experiences in Teams applications. ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/csharp.incl.md ================================================ ```csharp using Microsoft.Teams.Cards; var action = new ExecuteAction { Title = "Submit Feedback", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_feedback" } } }), AssociatedInputs = AssociatedInputs.Auto }; ``` ```csharp using Microsoft.Teams.Cards; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Actions = new List { new ExecuteAction { Title = "Submit Feedback", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_feedback" } } }) }, new OpenUrlAction("https://adaptivecards.microsoft.com") { Title = "Learn More" } } }; ``` N/A ```csharp var actionJson = """ { "type": "Action.OpenUrl", "url": "https://adaptivecards.microsoft.com", "title": "Learn More" } """; var action = OpenUrlAction.Deserialize(actionJson); ``` ```csharp private static AdaptiveCard CreateProfileCard() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("User Profile") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextInput { Id = "name", Label = "Name", Value = "John Doe" }, new TextInput { Id = "email", Label = "Email", Value = "john@contoso.com" }, new ToggleInput("Subscribe to newsletter") { Id = "subscribe", Value = "false" } }, Actions = new List { new ExecuteAction { Title = "Save", // entity_id will come back after the user submits Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "save_profile" }, { "entity_id", "12345" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; } // Data received in handler (conceptual structure) /* { "action": "save_profile", "entity_id": "12345", // From action data "name": "John Doe", // From name input "email": "john@doe.com", // From email input "subscribe": "true" // From toggle input (as string) } Accessed in C# as: - data["action"] → "save_profile" - data["entity_id"] → "12345" - data["name"] → "John Doe" - data["email"] → "john@doe.com" - data["subscribe"] → "true" */ ``` ```csharp private static AdaptiveCard CreateProfileCardWithValidation() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Profile with Validation") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new NumberInput { Id = "age", Label = "Age", IsRequired = true, Min = 0, Max = 120 }, // Can configure custom error messages new TextInput { Id = "name", Label = "Name", IsRequired = true, ErrorMessage = "Name is required" }, new TextInput { Id = "location", Label = "Location" } }, Actions = new List { new ExecuteAction { Title = "Save", // All inputs should be validated Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "save_profile" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; } ``` ```csharp using System.Text.Json; using Microsoft.Teams.Api.Activities.Invokes.AdaptiveCards; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... teams.OnAdaptiveCardAction(async (context, cancellationToken) => { var activity = context.Activity; context.Log.Info("[CARD_ACTION] Card action received"); var data = activity.Value?.Action?.Data; context.Log.Info($"[CARD_ACTION] Raw data: {JsonSerializer.Serialize(data)}"); if (data == null) { context.Log.Error("[CARD_ACTION] No data in card action"); return new ActionResponse.Message("No data specified") { StatusCode = 400 }; } string? action = data.TryGetValue("action", out var actionObj) ? actionObj?.ToString() : null; if (string.IsNullOrEmpty(action)) { context.Log.Error("[CARD_ACTION] No action specified in card data"); return new ActionResponse.Message("No action specified") { StatusCode = 400 }; } context.Log.Info($"[CARD_ACTION] Processing action: {action}"); string? GetFormValue(string key) { if (data.TryGetValue(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val?.ToString(); } return null; } switch (action) { case "submit_basic": var notifyValue = GetFormValue("notify") ?? "false"; await context.Send($"Basic card submitted! Notify setting: {notifyValue}", cancellationToken); break; case "submit_feedback": var feedbackText = GetFormValue("feedback") ?? "No feedback provided"; await context.Send($"Feedback received: {feedbackText}", cancellationToken); break; case "create_task": var title = GetFormValue("title") ?? "Untitled"; var priority = GetFormValue("priority") ?? "medium"; var dueDate = GetFormValue("due_date") ?? "No date"; await context.Send($"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {dueDate}", cancellationToken); break; case "save_profile": var name = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; var subscribe = GetFormValue("subscribe") ?? "false"; var age = GetFormValue("age"); var location = GetFormValue("location") ?? "Not specified"; var response = $"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}"; if (!string.IsNullOrEmpty(age)) response += $"\nAge: {age}"; if (location != "Not specified") response += $"\nLocation: {location}"; await context.Send(response, cancellationToken); break; case "test_json": await context.Send("JSON deserialization test successful!", cancellationToken); break; default: context.Log.Error($"[CARD_ACTION] Unknown action: {action}"); return new ActionResponse.Message("Unknown action") { StatusCode = 400 }; } return new ActionResponse.Message("Action processed successfully") { StatusCode = 200 }; }); ``` :::note The `data` values come from JSON and need to be extracted using the helper method shown above to handle different JSON element types. ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/python.incl.md ================================================ ```python from microsoft_teams.cards.core import ExecuteAction # ... action = ExecuteAction(title="Submit Feedback") .with_data({"action": "submit_feedback"}) .with_associated_inputs("auto") ``` ```python from microsoft_teams.cards.core import ActionSet, ExecuteAction, OpenUrlAction # ... action_set = ActionSet( actions=[ ExecuteAction(title="Submit Feedback") .with_data({"action": "submit_feedback"}), OpenUrlAction(url="https://adaptivecards.microsoft.com").with_title("Learn More") ] ), ``` You get type safety for free in Python. ```python json = { "type": "Action.OpenUrl", "url": "https://adaptivecards.microsoft.com", "title": "Learn More", } ``` ```python from microsoft_teams.cards import AdaptiveCard, ActionSet, ExecuteAction, OpenUrlAction from microsoft_teams.cards.core import TextInput, ToggleInput # ... profile_card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextInput(id="name").with_label("Name").with_value("John Doe"), TextInput(id="email", label="Email", value="john@contoso.com"), ToggleInput(title="Subscribe to newsletter").with_id("subscribe").with_value("false"), ActionSet( actions=[ ExecuteAction(title="Save") # entity_id will come back after the user submits .with_data({"action": "save_profile", "entity_id": "12345"}), ] ), ], ) # Data received in handler: """ { "action": "save_profile", "entity_id": "12345", # From action data "name": "John Doe", # From name input "email": "john@doe.com", # From email input "subscribe": "true" # From toggle input (as string) } """ ``` ```python from microsoft_teams.cards import AdaptiveCard, ActionSet, ExecuteAction, NumberInput, TextInput # ... def create_profile_card_input_validation(): age_input = NumberInput(id="age").with_label("age").with_is_required(True).with_min(0).with_max(120) # Can configure custom error messages name_input = TextInput(id="name").with_label("Name").with_is_required(True).with_error_message("Name is required") card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ age_input, name_input, TextInput(id="location").with_label("Location"), ActionSet( actions=[ ExecuteAction(title="Save") # All inputs should be validated .with_data({"action": "save_profile"}) .with_associated_inputs("auto") ] ), ], ) return card ``` ```python from microsoft_teams.api import AdaptiveCardInvokeActivity, AdaptiveCardActionErrorResponse, AdaptiveCardActionMessageResponse, HttpError, InnerHttpError, AdaptiveCardInvokeResponse from microsoft_teams.apps import ActivityContext # ... @app.on_card_action async def handle_card_action(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: data = ctx.activity.value.action.data if not data.get("action"): return AdaptiveCardActionErrorResponse( status_code=400, type="application/vnd.microsoft.error", value=HttpError( code="BadRequest", message="No action specified", inner_http_error=InnerHttpError( status_code=400, body={"error": "No action specified"}, ), ), ) print("Received action data:", data) if data["action"] == "submit_feedback": await ctx.send(f"Feedback received: {data.get('feedback')}") elif data["action"] == "purchase_item": await ctx.send(f"Purchase request received for game: {data.get('choiceGameSingle')}") elif data["action"] == "save_profile": await ctx.send( f"Profile saved!\nName: {data.get('name')}\nEmail: {data.get('email')}\nSubscribed: {data.get('subscribe')}" ) else: return AdaptiveCardActionErrorResponse( status_code=400, type="application/vnd.microsoft.error", value=HttpError( code="BadRequest", message="Unknown action", inner_http_error=InnerHttpError( status_code=400, body={"error": "Unknown action"}, ), ), ) return AdaptiveCardActionMessageResponse( status_code=200, type="application/vnd.microsoft.activity.message", value="Action processed successfully", ) ``` :::note The `data` values are accessible as a dictionary and can be accessed using `.get()` method for safe access. ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/typescript.incl.md ================================================ ```typescript import { ExecuteAction } from '@microsoft/teams.cards'; // ... new ExecuteAction({ title: 'Submit Feedback' }) .withData({ action: 'submit_feedback' }) .withAssociatedInputs('auto'), ``` ```typescript import { ExecuteAction, OpenUrlAction, ActionSet } from '@microsoft/teams.cards'; // ... new ActionSet( new ExecuteAction({ title: 'Submit Feedback' }) .withData({ action: 'submit_feedback' }) .withAssociatedInputs('auto'), new OpenUrlAction('https://adaptivecards.microsoft.com').withTitle('Learn More') ); ``` You get type safety for free in TypeScript. ```typescript import { IOpenUrlAction } from '@microsoft/teams.cards'; // ... { type: 'Action.OpenUrl', url: 'https://adaptivecards.microsoft.com', title: 'Learn More', } as const satisfies IOpenUrlAction ``` ```typescript import { AdaptiveCard, TextInput, ToggleInput, ActionSet, ExecuteAction, } from '@microsoft/teams.cards'; // ... function editProfileCard() { const card = new AdaptiveCard( new TextInput({ id: 'name' }).withLabel('Name').withValue('John Doe'), new TextInput({ id: 'email', label: 'Email', value: 'john@contoso.com' }), new ToggleInput('Subscribe to newsletter').withId('subscribe').withValue('false'), new ActionSet( new ExecuteAction({ title: 'Save' }) .withData({ action: 'save_profile', entityId: '12345', // This will come back once the user submits }) .withAssociatedInputs('auto') ) ); // Data received in handler /** { action: "save_profile", entityId: "12345", // From action data name: "John Doe", // From name input email: "john@doe.com", // From email input subscribe: "true" // From toggle input (as string) } */ return card; } ``` ```typescript import { AdaptiveCard, NumberInput, TextInput, ActionSet, ExecuteAction, } from '@microsoft/teams.cards'; // ... function createProfileCardInputValidation() { const ageInput = new NumberInput({ id: 'age' }) .withLabel('Age') .withIsRequired(true) .withMin(0) .withMax(120); const nameInput = new TextInput({ id: 'name' }) .withLabel('Name') .withIsRequired() .withErrorMessage('Name is required!'); // Custom error messages const card = new AdaptiveCard( nameInput, ageInput, new TextInput({ id: 'location' }).withLabel('Location'), new ActionSet( new ExecuteAction({ title: 'Save' }) .withData({ action: 'save_profile', }) .withAssociatedInputs('auto') // All inputs should be validated ) ); return card; } ``` ```typescript import { AdaptiveCardActionErrorResponse, AdaptiveCardActionMessageResponse, } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... app.on('card.action', async ({ activity, send }) => { const data = activity.value?.action?.data; if (!data?.action) { return { statusCode: 400, type: 'application/vnd.microsoft.error', value: { code: 'BadRequest', message: 'No action specified', innerHttpError: { statusCode: 400, body: { error: 'No action specified' }, }, }, } satisfies AdaptiveCardActionErrorResponse; } console.debug('Received action data:', data); switch (data.action) { case 'submit_feedback': await send(`Feedback received: ${data.feedback}`); break; case 'purchase_item': await send(`Purchase request received for game: ${data.choiceGameSingle}`); break; case 'save_profile': await send( `Profile saved!\nName: ${data.name}\nEmail: ${data.email}\nSubscribed: ${data.subscribe}` ); break; default: return { statusCode: 400, type: 'application/vnd.microsoft.error', value: { code: 'BadRequest', message: 'Unknown action', innerHttpError: { statusCode: 400, body: { error: 'Unknown action' }, }, }, } satisfies AdaptiveCardActionErrorResponse; } return { statusCode: 200, type: 'application/vnd.microsoft.activity.message', value: 'Action processed successfully', } satisfies AdaptiveCardActionMessageResponse; }); ``` :::note The `data` values are not typed and come as `any`, so you will need to cast them to the correct type in this case. ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/python.incl.md ================================================ Overview of Adaptive Cards in Python Teams SDK for building rich, interactive user experiences in Teams applications. ================================================ FILE: teams.md/src/components/include/in-depth-guides/adaptive-cards/typescript.incl.md ================================================ Overview of Adaptive Cards in TypeScript Teams SDK for building rich, interactive user experiences in Teams applications. ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/python.incl.md ================================================ ```python import uuid from httpx import AsyncClient from a2a.types import AgentCard, Message, Part, Role, Task, TextPart from a2a.client import ClientFactory, Client from a2a.client.card_resolver import A2ACardResolver from a2a.client.client import ClientConfig from .agent_client_info import AgentClientInfo from .agent_config import AgentConfig # Retrive the agent card based on the URL async def fetch_agent_card(self, base_url: str, card_url: str) -> AgentCard: async with AsyncClient() as httpx_client: resolver = A2ACardResolver(httpx_client, base_url) card = await resolver.get_agent_card(card_url) return card async def _get_client(self, key: str, config: AgentConfig) -> Client: try: # Example base_url=f"http://localhost:{PORT}/a2a" # Example card_url=".well-known/agent-card.json" card = await self.fetch_agent_card(base_url=config.base_url, card_url=config.card_url) client_config = ClientConfig() # Create the client from the URL client = ClientFactory(client_config).create(card=card) client_info = AgentClientInfo(**asdict(config), client=client, agent_card=card) self._clients.update({key: client_info}) return client except Exception as e: self.log.error(f"Error creating client or fetching agent card for {key}: {e}") raise e async def _send_message(self) -> None: # Send a message directly message = Message( message_id=str(uuid.uuid4()), role=Role("user"), parts=[Part(root=TextPart(kind="text", text="What is the weather?"))], ) async for event in self._get_client().send_message(message): # Handle the event ``` ```python from os import getenv from microsoft_teams.openai.completions_model import OpenAICompletionsAIModel from microsoft_teams.a2a import A2AClientPlugin, A2APluginUseParams from microsoft_teams.ai import ChatPrompt PORT = getenv("PORT", "4000") # Setup AI def get_required_env(key: str) -> str: value = getenv(key) if not value: raise ValueError(f"Required environment variable {key} is not set") return value AZURE_OPENAI_MODEL = get_required_env("AZURE_OPENAI_MODEL") completions_model = OpenAICompletionsAIModel(model=AZURE_OPENAI_MODEL) # Setup A2A Client Plugin client_plugin = A2AClientPlugin() # Specify the connection details for the agent we want to use client_plugin.on_use_plugin( A2APluginUseParams( key="my-weather-agent", base_url=f"http://localhost:{PORT}/a2a", card_url=".well-known/agent-card.json" ) ) prompt = ChatPrompt( model=completions_model, plugins=[client_plugin], ) ``` ```python # Now we can send the message to the prompt and it will decide if # the a2a agent should be used or not and also manages contacting the agent result = await prompt.send(message) ``` ```python # Example with custom message builders and response processors def build_function_metadata(card: AgentCard) -> FunctionMetadata: return FunctionMetadata( name=f"ask{re.sub(r'\s+', '', card.name)}", description=f"Ask {card.name} about {card.description or 'anything'}", ) def build_message_for_agent(data: BuildMessageForAgentMetadata) -> Union[Message, str]: # Return a string - will be automatically wrapped in a Message return f"[To {data.card.name}]: {data.input}" # Uncomment the following block to return a full Message object # message = Message( # kind='message', # message_id=str(uuid4()), # role=Role('user'), # parts=[Part(root=TextPart(kind='text', text=f"[To {data.card.name}]: {data.input}"))], # metadata={"source": "chat-prompt", **(data.metadata if data.metadata else {})} # ) # return message def build_message_from_agent_response(data: BuildMessageFromAgentMetadata) -> str: if isinstance(data.response, Message): text_parts: List[str] = [] for part in data.response.parts: if getattr(part.root, "kind", None) == "text": text_part = cast(TextPart, part.root) text_parts.append(text_part.text) return f"{data.card.name} says: {' '.join(text_parts)}" return f"{data.card.name} sent a non-text response." ## Advanced A2AClientPlugin advanced_plugin = A2AClientPlugin( # Custom function metadata builder build_function_metadata=build_function_metadata, # Custom message builder - can return either Message or string build_message_for_agent=build_message_for_agent, # Custom response processor build_message_from_agent_response=build_message_from_agent_response, ) advanced_plugin.on_use_plugin( A2APluginUseParams( key="my-weather-agent", base_url=f"http://localhost:{PORT}/a2a", card_url=".well-known/agent-card.json" ) ) advanced_prompt = ChatPrompt(model=completions_model, plugins=[advanced_plugin]) ``` ```mermaid sequenceDiagram participant User participant ChatPrompt participant A2AClientPlugin participant A2ACardResolver participant Client participant LLM participant A2AServer Note over User,A2AServer: Configuration User->>A2AClientPlugin: on_use_plugin() Note over User,A2AServer: Message Flow User->>ChatPrompt: send(message) ChatPrompt->>A2AClientPlugin: on_build_instructions() A2AClientPlugin->>A2ACardResolver: fetch_agent_card() A2ACardResolver->>A2AServer: GET /.well-known/agent-card.json A2AServer-->>A2ACardResolver: AgentCard A2ACardResolver-->>A2AClientPlugin: AgentCard A2AClientPlugin-->>ChatPrompt: Enhanced system prompt ChatPrompt->>A2AClientPlugin: on_build_functions() A2AClientPlugin-->>ChatPrompt: Function tools for agents ChatPrompt->>LLM: Enhanced prompt + tools LLM-->>ChatPrompt: Function call (message_agent) ChatPrompt->>A2AClientPlugin: Execute function handler A2AClientPlugin->>Client: send_message() Client->>A2AServer: POST /a2a/task/send A2AServer-->>Client: Response Client-->>A2AClientPlugin: Response A2AClientPlugin-->>ChatPrompt: Processed response ChatPrompt-->>User: Final response ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/typescript.incl.md ================================================ ```typescript import { A2AClient } from '@a2a-js/sdk/client'; // Create client from agent card URL const client = await A2AClient.fromCardUrl('http://localhost:4000/a2a/.well-known/agent-card.json'); // Send a message directly const response = await client.sendMessage({ message: { messageId: 'unique-id', role: 'user', parts: [{ kind: 'text', text: 'What is the weather?' }], kind: 'message', }, }); ``` ```typescript import { A2AClientPlugin } from '@microsoft/teams.a2a'; import { ChatPrompt } from '@microsoft/teams.ai'; import { OpenAIChatModel } from '@microsoft/teams.openai'; const prompt = new ChatPrompt( { model: new OpenAIChatModel({ apiKey: process.env.AZURE_OPENAI_API_KEY, model: process.env.AZURE_OPENAI_MODEL!, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: process.env.AZURE_OPENAI_API_VERSION, }), }, // Add the A2AClientPlugin to the prompt [new A2AClientPlugin()] ) // Provide the agent's card URL .usePlugin('a2a', { key: 'my-weather-agent', cardUrl: 'http://localhost:4000/a2a/.well-known/agent-card.json', }); ``` ```typescript // Now we can send the message to the prompt and it will decide if // the a2a agent should be used or not and also manages contacting the agent const result = await prompt.send(message); ``` ```typescript // Example with custom message builders and response processors export const advancedPrompt = new ChatPrompt( { model: new OpenAIChatModel({ apiKey: process.env.AZURE_OPENAI_API_KEY, model: process.env.AZURE_OPENAI_MODEL!, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: process.env.AZURE_OPENAI_API_VERSION, }), }, [ new A2AClientPlugin({ // Custom function metadata builder buildFunctionMetadata: (card) => ({ name: `ask${card.name.replace(/\s+/g, '')}`, description: `Ask ${card.name} about ${card.description || 'anything'}`, }), // Custom message builder - can return either Message or string buildMessageForAgent: (card, input) => { // Return a string - will be automatically wrapped in a Message return `[To ${card.name}]: ${input}`; }, // Custom response processor buildMessageFromAgentResponse: (card, response) => { if (response.kind === 'message') { const textParts = response.parts .filter((part) => part.kind === 'text') .map((part) => part.text); return `${card.name} says: ${textParts.join(' ')}`; } return `${card.name} sent a non-text response.`; }, }), ] ).usePlugin('a2a', { key: 'weather-agent', cardUrl: 'http://localhost:4000/a2a/.well-known/agent-card.json', }); ``` ```mermaid sequenceDiagram participant User participant ChatPrompt participant A2AClientPlugin participant A2AClient participant LLM participant A2AServer Note over User,A2AServer: Configuration User->>ChatPrompt: usePlugin('a2a', {cardUrl}) ChatPrompt->>A2AClientPlugin: onUsePlugin() Note over User,A2AServer: Message Flow User->>ChatPrompt: send(message) ChatPrompt->>A2AClientPlugin: onBuildPrompt() A2AClientPlugin->>A2AClient: getAgentCard() A2AClient->>A2AServer: GET /.well-known/agent-card.json A2AServer-->>A2AClient: AgentCard A2AClient-->>A2AClientPlugin: AgentCard A2AClientPlugin-->>ChatPrompt: Enhanced system prompt ChatPrompt->>A2AClientPlugin: onBuildFunctions() A2AClientPlugin-->>ChatPrompt: Function tools for agents ChatPrompt->>LLM: Enhanced prompt + tools LLM-->>ChatPrompt: Function call (messageAgent) ChatPrompt->>A2AClientPlugin: Execute function handler A2AClientPlugin->>A2AClient: sendMessage() A2AClient->>A2AServer: POST /a2a/task/send A2AServer-->>A2AClient: Response A2AClient-->>A2AClientPlugin: Response A2AClientPlugin-->>ChatPrompt: Processed response ChatPrompt-->>User: Final response ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/python.incl.md ================================================ `agent_card` ```python from os import getenv from a2a.types import AgentCard, AgentCapabilities, AgentSkill from microsoft_teams.a2a import A2APlugin, A2APluginOptions from microsoft_teams.apps import App, PluginBase PORT = getenv("PORT", "4000") agent_card = AgentCard( name="weather_agent", description="An agent that can tell you the weather", url=f"http://localhost:{PORT}/a2a/", version="0.0.1", protocol_version="0.3.0", capabilities=AgentCapabilities(), default_input_modes=[], default_output_modes=[], skills=[ AgentSkill( # Expose various skills that this agent can perform id="get_weather", name="Get Weather", description="Get the weather for a given location", tags=["weather", "get", "location"], examples=[ # Give concrete examples on how to contact the agent "Get the weather for London", "What is the weather", "What's the weather in Tokyo?", "How is the current temperature in San Francisco?", ], ), ], ) plugins: List[PluginBase] = [A2APlugin(A2APluginOptions(agent_card=agent_card))] app = App(logger=logger, plugins=plugins) ``` ```python from microsoft_teams.a2a import A2AMessageEvent, A2AMessageEventKey from a2a.types import TextPart @app.event(A2AMessageEventKey) async def handle_a2a_message(message: A2AMessageEvent) -> None: request_context = message.get("request_context") respond = message.get("respond") logger.info(f"Received message: {request_context.message}") if request_context.message: text_input = None for part in request_context.message.parts: if getattr(part.root, "kind", None) == "text": text_part = cast(TextPart, part.root) text_input = text_part.text break if not text_input: await respond("My agent currently only supports text input") return result = await my_event_handler(text_input) await respond(result) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/typescript.incl.md ================================================ `agentCard` ```typescript import { AgentCard } from '@a2a-js/sdk'; import { A2APlugin } from '@microsoft/teams.a2a'; import { App } from '@microsoft/teams.apps'; const agentCard: AgentCard = { name: 'Weather Agent', description: 'An agent that can tell you the weather', url: `http://localhost:${PORT}/a2a`, version: '0.0.1', protocolVersion: '0.3.0', capabilities: {}, defaultInputModes: [], defaultOutputModes: [], skills: [ { id: 'get_weather', name: 'Get Weather', description: 'Get the weather for a given location', tags: ['weather', 'get', 'location'], examples: [ 'Get the weather for London', 'What is the weather', "What's the weather in Tokyo?", 'How is the current temperature in San Francisco?', ], }, ], }; const app = new App({ plugins: [ new A2APlugin({ agentCard, }), ], }); ``` ```typescript app.event('a2a:message', async ({ respond, requestContext }) => { logger.info(`Received message: ${requestContext.userMessage}`); const textInput = requestContext.userMessage.parts .filter((p): p is TextPart => p.kind === 'text') .at(0)?.text; if (!textInput) { await respond('My agent currently only supports text input'); return; } const result = await myEventHandler(textInput); await respond(result); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/python.incl.md ================================================ :::note This package wraps the official [A2A SDK](https://github.com/a2aproject/a2a-python) for both server and client. ::: Install the package: ```bash pip install microsoft-teams-a2a ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/a2a/typescript.incl.md ================================================ :::note This package wraps the official [A2A SDK](https://github.com/a2aproject/a2a-js) for both server and client. ::: Install the package: ```bash npm install @microsoft/teams.a2a ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/best-practices/csharp.incl.md ================================================ This can be done by calling the `.AddAIGenerated()` method on outgoing messages. ```csharp var messageActivity = new MessageActivity { Text = "Hello!", }.AddAIGenerated(); ``` This is easy to do by using the `AddCitation` method on the message. ```csharp var messageActivity = new MessageActivity { Text = result.Content, }.AddAIGenerated(); for (int i = 0; i < citedDocs.Length; i++) { messageActivity.Text += $"[{i + 1}]"; messageActivity.AddCitation(i + 1, new CitationAppearance { Name = citedDocs[i].Title, Abstract = citedDocs[i].Content }); } ``` You can do that by using the `WithSuggestedActions` method on the message. ```csharp var message = new MessageActivity { Text = result.Content, }.WithSuggestedActions( new Microsoft.Teams.Api.SuggestedActions() { To = [context.Activity.From.Id], Actions = [ new Microsoft.Teams.Api.Cards.Action(ActionType.IMBack) { Title = "Thank you!", Value = "Thank you very much!" } ] }).AddAIGenerated(); await context.Send(message); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/best-practices/python.incl.md ================================================ This can be done by adding a `addAiGenerated` property to outgoing message. ```python message_to_be_sent = MessageActivityInput(text="Hello!").add_ai_generated() ``` This is easy to do by simply using the `addCitations` method on the message. This will add a citation to the message, and the LLM will be able to use it to generate a citation for the user. ```python from microsoft_teams.api import MessageActivityInput, CitationAppearance message_activity = MessageActivityInput(text=result.content).add_ai_generated() for i, doc in enumerate(cited_docs): message_activity.text += f"[{i + 1}]" message_activity.add_citation(i + 1, CitationAppearance(name=doc["title"], abstract=doc["content"])) ``` You can do that by using the `with_suggested_actions` method on the message. ```python from microsoft_teams.api import CardAction, CardActionType, MessageActivityInput, SuggestedActions suggested_actions = SuggestedActions( to=[activity.from_.id], actions=[CardAction(type=CardActionType.IM_BACK, title="Thanks!", value="Thank you so much!")], ) message = ( MessageActivityInput(text=chat_result.response.content) .add_ai_generated() .with_suggested_actions(suggested_actions) ) await ctx.send(message) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/best-practices/typescript.incl.md ================================================ This can be done by adding a `addAiGenerated` property to outgoing message. ```typescript const messageToBeSent = new MessageActivity('Hello!').addAiGenerated(); ``` This is easy to do by simply using the `addCitations` method on the message. This will add a citation to the message, and the LLM will be able to use it to generate a citation for the user. ```typescript import { MessageActivity } from '@microsoft/teams.api'; // ... const messageActivity = new MessageActivity(result.content).addAiGenerated(); for (let i = 0; i < citedDocs.length; i++) { const doc = citedDocs[i]; // The corresponding citation needs to be added in the message content messageActivity.text += `[${i + 1}]`; messageActivity.addCitation(i + 1, { name: doc.title, abstract: doc.content, }); } ``` You can do that by using the `withSuggestedActions` method on the message. ```typescript message.withSuggestedActions({ to: [activity.from.id], actions: [ { type: 'imBack', title: 'Show pricing options', value: 'Show the pricing options available to me', }, ], }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/chat/csharp.incl.md ================================================ Import the relevant namespaces: ```csharp // AI using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; // Teams using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Annotations; ``` Create a ChatModel, ChatPrompt, and handle user - LLM interactions: ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps.Activities; using Azure.AI.OpenAI; using System.ClientModel; // Configuration var azureOpenAIModel = configuration["AzureOpenAIModel"]!; var azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"]!; var azureOpenAIKey = configuration["AzureOpenAIKey"]!; var azureOpenAI = new AzureOpenAIClient( new Uri(azureOpenAIEndpoint), new ApiKeyCredential(azureOpenAIKey) ); // AI Model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Simple chat handler teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = new OpenAIChatPrompt(aiModel, new ChatPromptOptions { Instructions = new StringTemplate("You are a friendly assistant who talks like a pirate") }); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var messageActivity = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(messageActivity, cancellationToken); // Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? What can this ol' salty sea dog help ye with? 🚢☠️ } }); ``` ### Declarative Approach This approach uses attributes to declare prompts, providing clean separation of concerns. **Create a Prompt Class:** ```csharp using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("A friendly pirate assistant")] [Prompt.Instructions("You are a friendly assistant who talks like a pirate")] public class PiratePrompt { } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.PiratePrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); // Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? } }); ``` :::note The current `OpenAIChatModel` implementation uses chat-completions API. The responses API is coming soon. ::: N/A ```csharp // Streaming handler teamsApp.OnMessage(async (context, cancellationToken) => { var match = Regex.Match(context.Activity.Text ?? "", @"^stream\s+(.+)", RegexOptions.IgnoreCase); if (match.Success) { var query = match.Groups[1].Value.Trim(); var prompt = new OpenAIChatPrompt(aiModel, new ChatPromptOptions { Instructions = new StringTemplate("You are a friendly assistant who responds in extremely verbose language") }); var result = await prompt.Send(query, (chunk) => { context.Stream.Emit(chunk); return Task.CompletedTask; }); } }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/chat/python.incl.md ================================================ Import the relevant objects: ```python from microsoft_teams.ai import ChatPrompt from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext from microsoft_teams.openai import OpenAICompletionsAIModel ``` ```python @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): openai_model = OpenAICompletionsAIModel(model=AZURE_OPENAI_MODEL) agent = ChatPrompt(model=openai_model) chat_result = await agent.send( input=ctx.activity.text, instructions="You are a friendly assistant who talks like a pirate." ) result = chat_result.response if result.content: await ctx.send(MessageActivityInput(text=result.content).add_ai_generated()) # Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? What can this ol' salty sea dog help ye with? 🚢☠️ ``` N/A :::note The current `OpenAICompletionsAIModel` implementation uses Chat Completions API. The Responses API is also available. ::: ### Agent Instead of `ChatPrompt`, you may also use `Agent`. The `Agent` class is a derivation from `ChatPrompt` but it differs in that it's stateful. The `memory` object passed to the `Agent` object will be reused for subsequent calls to `send`, whereas for `ChatPrompt`, each call to `send` is independent. ```python from microsoft_teams.ai import ChatPrompt from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext from microsoft_teams.openai import OpenAICompletionsAIModel # ... @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): openai_model = OpenAICompletionsAIModel(model=AZURE_OPENAI_MODEL) agent = ChatPrompt(model=openai_model) chat_result = await agent.send( input=ctx.activity.text, instructions="You are a friendly assistant who responds in terse language.", on_chunk=lambda chunk: ctx.stream.emit(chunk) ) result = chat_result.response if ctx.activity.conversation.is_group: # If the conversation is a group chat, we need to send the final response # back to the group chat await ctx.send(MessageActivityInput(text=result.content).add_ai_generated()) else: ctx.stream.emit(MessageActivityInput().add_ai_generated()) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/chat/typescript.incl.md ================================================ Import the relevant objects: ```typescript import { OpenAIChatModel } from '@microsoft/teams.openai'; ``` ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { OpenAIChatModel } from '@microsoft/teams.openai'; // ... app.on('message', async ({ send, activity, next, log }) => { const model = new OpenAIChatModel({ apiKey: process.env.AZURE_OPENAI_API_KEY || process.env.OPENAI_API_KEY, endpoint: process.env.AZURE_OPENAI_ENDPOINT, apiVersion: process.env.AZURE_OPENAI_API_VERSION, model: process.env.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME!, }); const prompt = new ChatPrompt({ instructions: 'You are a friendly assistant who talks like a pirate', model, }); const response = await prompt.send(activity.text); if (response.content) { const activity = new MessageActivity(response.content).addAiGenerated(); await send(activity); // Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? What can this ol' salty sea dog help ye with? 🚢☠️ } }); ``` N/A :::note The current `OpenAIChatModel` implementation uses chat-completions API. The responses API is coming soon. ::: N/A ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... app.on('message', async ({ stream, send, activity, next, log }) => { // const query = activity.text; const prompt = new ChatPrompt({ instructions: 'You are a friendly assistant who responds in extremely verbose language', model, }); // Notice that we don't `send` the final response back, but // `stream` the chunks as they come in const response = await prompt.send(query, { onChunk: (chunk) => { stream.emit(chunk); }, }); if (activity.conversation.isGroup) { // If the conversation is a group chat, we need to send the final response // back to the group chat const activity = new MessageActivity(response.content).addAiGenerated(); await send(activity); } else { // We wrap the final response with an AI Generated indicator stream.emit(new MessageActivity().addAiGenerated()); } }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/csharp.incl.md ================================================ `Microsoft.Teams.AI` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/function-calling/csharp.incl.md ================================================ registering functions with a `ChatPrompt` using the `.Function()` method ```mermaid sequenceDiagram participant User participant ChatPrompt participant LLM participant Function-PokemonSearch participant ExternalAPI User->>ChatPrompt: send("Tell me about Pikachu") ChatPrompt->>LLM: Provide instructions, message, and available functions LLM->>ChatPrompt: Decide to call `pokemon_search` with pokemon_name="Pikachu" ChatPrompt->>Function-PokemonSearch: Execute with pokemon_name Function-PokemonSearch->>ExternalAPI: fetch Pokemon data ExternalAPI-->>Function-PokemonSearch: return Pokemon info Function-PokemonSearch-->>ChatPrompt: return result ChatPrompt->>LLM: Send function result(s) LLM-->>ChatPrompt: Final user-facing response ChatPrompt-->>User: send(result.content) ``` ## Single Function Example Here's a complete example showing how to create a Pokemon search function that the LLM can call. import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```csharp using System.Text.Json; using Microsoft.Teams.AI.Annotations; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; /// /// Handle Pokemon search using PokeAPI /// public static async Task PokemonSearchFunction([Param("pokemon_name")] string pokemonName) { try { using var client = new HttpClient(); var response = await client.GetAsync($"https://pokeapi.co/api/v2/pokemon/{pokemonName.ToLower()}"); if (!response.IsSuccessStatusCode) { return $"Pokemon '{pokemonName}' not found"; } var json = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(json); var root = data.RootElement; var name = root.GetProperty("name").GetString(); var height = root.GetProperty("height").GetInt32(); var weight = root.GetProperty("weight").GetInt32(); var types = root.GetProperty("types") .EnumerateArray() .Select(t => t.GetProperty("type").GetProperty("name").GetString()) .ToList(); return $"Pokemon {name}: height={height}, weight={weight}, types={string.Join(", ", types)}"; } catch (Exception ex) { return $"Error searching for Pokemon: {ex.Message}"; } } /// /// Handle single function calling - Pokemon search /// public static async Task HandlePokemonSearch(OpenAIChatModel model, IContext context) { var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that can look up Pokemon for the user.") }); // Register the pokemon search function prompt.Function( "pokemon_search", "Search for pokemon information including height, weight, and types", PokemonSearchFunction ); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); } else { await context.Reply("Sorry I could not find that pokemon"); } } ``` This approach uses attributes to declare prompts and functions, providing clean separation of concerns. **Create a Prompt Class:** ```csharp using System.Text.Json; using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("Pokemon search assistant")] [Prompt.Instructions("You are a helpful assistant that can look up Pokemon for the user.")] public class PokemonPrompt { [Function] [Function.Description("Search for pokemon information including height, weight, and types")] public async Task PokemonSearch([Param("pokemon_name")] string pokemonName) { try { using var httpClient = new HttpClient(); var response = await httpClient.GetAsync($"https://pokeapi.co/api/v2/pokemon/{pokemonName.ToLower()}"); if (!response.IsSuccessStatusCode) { return $"Pokemon '{pokemonName}' not found"; } var json = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(json); var root = data.RootElement; var name = root.GetProperty("name").GetString(); var height = root.GetProperty("height").GetInt32(); var weight = root.GetProperty("weight").GetInt32(); var types = root.GetProperty("types") .EnumerateArray() .Select(t => t.GetProperty("type").GetProperty("name").GetString()) .ToList(); return $"Pokemon {name}: height={height}, weight={weight}, types={string.Join(", ", types)}"; } catch (Exception ex) { return $"Error searching for Pokemon: {ex.Message}"; } } } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.PokemonPrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); } else { await context.Reply("Sorry I could not find that pokemon", cancellationToken); } }); ``` ### How It Works 1. **Function Definition**: The function is defined as a regular C# method with parameters decorated with the `[Param]` attribute 2. **Automatic Schema Generation**: The SDK automatically generates the JSON schema for the function parameters using reflection 3. **Function Registration**: - **Imperative Approach**: The `.Function()` method registers the function with the prompt, providing the name, description, and handler - **Declarative Approach**: The `[Function]` attribute automatically registers methods when using `OpenAIChatPrompt.From()` 4. **Automatic Invocation**: When the LLM decides to call the function, it automatically: - Parses the function call arguments - Validates them against the schema - Invokes the handler - Returns the result back to the LLM ## Multiple Functions Additionally, for complex scenarios, you can add multiple functions to the `ChatPrompt`. The LLM will then decide which function(s) to call based on the context of the conversation. ```csharp /// /// Get user location (mock) /// public static string GetLocationFunction() { var locations = new[] { "Seattle", "San Francisco", "New York" }; var random = new Random(); var location = locations[random.Next(locations.Length)]; return location; } /// /// Get weather for location (mock) /// public static string GetWeatherFunction([Param] string location) { var weatherByLocation = new Dictionary { ["Seattle"] = (65, "sunny"), ["San Francisco"] = (60, "foggy"), ["New York"] = (75, "rainy") }; if (!weatherByLocation.TryGetValue(location, out var weather)) { return "Sorry, I could not find the weather for that location"; } return $"The weather in {location} is {weather.Condition} with a temperature of {weather.Temperature}°F"; } /// /// Handle multiple function calling - location then weather /// public static async Task HandleMultipleFunctions(OpenAIChatModel model, IContext context) { var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that can help the user get the weather. First get their location, then get the weather for that location.") }); // Register both functions prompt.Function( "get_user_location", "Gets the location of the user", GetLocationFunction ); prompt.Function( "weather_search", "Search for weather at a specific location", GetWeatherFunction ); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); } else { await context.Reply("Sorry I could not figure it out"); } } ``` **Create a Prompt Class:** ```csharp using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("Weather assistant")] [Prompt.Instructions("You are a helpful assistant that can help the user get the weather. First get their location, then get the weather for that location.")] public class WeatherPrompt { [Function] [Function.Description("Gets the location of the user")] public string GetUserLocation() { var locations = new[] { "Seattle", "San Francisco", "New York" }; var random = new Random(); return locations[random.Next(locations.Length)]; } [Function] [Function.Description("Search for weather at a specific location")] public string WeatherSearch([Param] string location) { var weatherByLocation = new Dictionary { ["Seattle"] = (65, "sunny"), ["San Francisco"] = (60, "foggy"), ["New York"] = (75, "rainy") }; if (!weatherByLocation.TryGetValue(location, out var weather)) { return "Sorry, I could not find the weather for that location"; } return $"The weather in {location} is {weather.Condition} with a temperature of {weather.Temperature}°F"; } } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.WeatherPrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); } else { await context.Reply("Sorry I could not figure it out", cancellationToken); } }); ``` ### Multiple Function Execution Flow When you register multiple functions: 1. The LLM receives information about all available functions 2. Based on the user's query, it decides which function(s) to call and in what order 3. For example, asking "What's the weather?" might trigger: - First: `get_user_location()` to determine where the user is - Then: `weather_search(location)` to get the weather for that location 4. The LLM combines all function results to generate the final response :::tip The LLM can call functions sequentially - using the output of one function as input to another - without any additional configuration. This makes it powerful for complex, multi-step workflows. ::: N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/function-calling/python.incl.md ================================================ adding a `function` to the `ChatPrompt` ```mermaid sequenceDiagram participant User participant ChatPrompt participant LLM participant Function-PokemonSearch participant ExternalAPI User->>ChatPrompt: send(activity.text) ChatPrompt->>LLM: Provide instructions, message, and available functions LLM->>ChatPrompt: Decide to call `pokemon_search` with pokemon_name ChatPrompt->>Function-PokemonSearch: Execute with pokemon_name Function-PokemonSearch->>ExternalAPI: fetch pokemon data ExternalAPI-->>Function-PokemonSearch: return pokemon info Function-PokemonSearch-->>ChatPrompt: return result ChatPrompt->>LLM: Send function result(s) LLM-->>ChatPrompt: Final user-facing response ChatPrompt-->>User: send(result.content) ``` ```python import aiohttp import random from microsoft_teams.ai import Agent, Function from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext from microsoft_teams.openai import OpenAICompletionsAIModel from pydantic import BaseModel class SearchPokemonParams(BaseModel): pokemon_name: str """The name of the pokemon.""" async def pokemon_search_handler(params: SearchPokemonParams) -> str: """Search for Pokemon using PokeAPI - matches documentation example""" try: async with aiohttp.ClientSession() as session: async with session.get(f"https://pokeapi.co/api/v2/pokemon/{params.pokemon_name.lower()}") as response: if response.status != 200: raise ValueError(f"Pokemon '{params.pokemon_name}' not found") data = await response.json() result_data = { "name": data["name"], "height": data["height"], "weight": data["weight"], "types": [type_info["type"]["name"] for type_info in data["types"]], } return f"Pokemon {result_data['name']}: height={result_data['height']}, weight={result_data['weight']}, types={', '.join(result_data['types'])}" except Exception as e: raise ValueError(f"Error searching for Pokemon: {str(e)}") @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): openai_model = OpenAICompletionsAIModel(model=AZURE_OPENAI_MODEL) agent = Agent(model=openai_model) agent.with_function( Function( name="pokemon_search", description="Search for pokemon information including height, weight, and types", # Include the schema of the parameters # the LLM needs to return to call the function parameter_schema=SearchPokemonParams, handler=pokemon_search_handler, ) ) chat_result = await agent.send( input=ctx.activity.text, instructions="You are a helpful assistant that can look up Pokemon for the user.", ) if chat_result.response.content: message = MessageActivityInput(text=chat_result.response.content).add_ai_generated() await ctx.send(message) else: await ctx.reply("Sorry I could not find that pokemon") ``` ## Multiple functions Additionally, for complex scenarios, you can add multiple functions to the `ChatPrompt`. The LLM will then decide which function to call based on the context of the conversation. The LLM can pick one or more functions to call before returning the final response. ```python import random from microsoft_teams.ai import Agent, Function from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext from pydantic import BaseModel # ... class GetLocationParams(BaseModel): """No parameters needed for location""" pass class GetWeatherParams(BaseModel): location: str """The location to get weather for""" def get_location_handler(params: GetLocationParams) -> str: """Get user location (mock)""" locations = ["Seattle", "San Francisco", "New York"] location = random.choice(locations) return location def get_weather_handler(params: GetWeatherParams) -> str: """Get weather for location (mock)""" weather_by_location = { "Seattle": {"temperature": 65, "condition": "sunny"}, "San Francisco": {"temperature": 60, "condition": "foggy"}, "New York": {"temperature": 75, "condition": "rainy"}, } weather = weather_by_location.get(params.location) if not weather: return "Sorry, I could not find the weather for that location" return f"The weather in {params.location} is {weather['condition']} with a temperature of {weather['temperature']}°F" @app.on_message async def handle_multiple_functions(ctx: ActivityContext[MessageActivity]): agent = Agent(model) agent.with_function( Function( name="get_user_location", description="Gets the location of the user", parameter_schema=GetLocationParams, handler=get_location_handler, ) ).with_function( Function( name="weather_search", description="Search for weather at a specific location", parameter_schema=GetWeatherParams, handler=get_weather_handler, ) ) chat_result = await agent.send( input=ctx.activity.text, instructions="You are a helpful assistant that can help the user get the weather. First get their location, then get the weather for that location.", ) if chat_result.response.content: message = MessageActivityInput(text=chat_result.response.content).add_ai_generated() await ctx.send(message) else: await ctx.reply("Sorry I could not figure it out") ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/function-calling/typescript.incl.md ================================================ adding a `function` to the `ChatPrompt` ```mermaid sequenceDiagram participant User participant ChatPrompt participant LLM participant Function-PokemonSearch participant ExternalAPI User->>ChatPrompt: send(activity.text) ChatPrompt->>LLM: Provide instructions, message, and available functions LLM->>ChatPrompt: Decide to call `pokemonSearch` with parameters ChatPrompt->>Function-PokemonSearch: Execute with pokemonName Function-PokemonSearch->>ExternalAPI: fetch pokemon data ExternalAPI-->>Function-PokemonSearch: return pokemon info Function-PokemonSearch-->>ChatPrompt: return result ChatPrompt->>LLM: Send function result(s) LLM-->>ChatPrompt: Final user-facing response ChatPrompt-->>User: send(result.content) ``` ```typescript import { ChatPrompt, IChatModel } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity } from '@microsoft/teams.api'; // ... const prompt = new ChatPrompt({ instructions: 'You are a helpful assistant that can look up Pokemon for the user.', model, }) // Include `function` as part of the prompt .function( 'pokemonSearch', 'search for pokemon', // Include the schema of the parameters // the LLM needs to return to call the function { type: 'object', properties: { pokemonName: { type: 'string', description: 'the name of the pokemon', }, }, required: ['text'], }, // The cooresponding function will be called // automatically if the LLM decides to call this function async ({ pokemonName }: IPokemonSearch) => { log.info('Searching for pokemon', pokemonName); const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`); if (!response.ok) { throw new Error('Pokemon not found'); } const data = await response.json(); // The result of the function call is sent back to the LLM return { name: data.name, height: data.height, weight: data.weight, types: data.types.map((type: { type: { name: string } }) => type.type.name), }; } ); // The LLM will then produce a final response to be sent back to the user // activity.text could have text like 'pikachu' const result = await prompt.send(activity.text); await send(result.content ?? 'Sorry I could not find that pokemon'); ``` ## Multiple functions Additionally, for complex scenarios, you can add multiple functions to the `ChatPrompt`. The LLM will then decide which function to call based on the context of the conversation. The LLM can pick one or more functions to call before returning the final response. ```typescript import { ChatPrompt, IChatModel } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity } from '@microsoft/teams.api'; // ... // activity.text could be something like "what's my weather?" // The LLM will need to first figure out the user's location // Then pass that in to the weatherSearch const prompt = new ChatPrompt({ instructions: 'You are a helpful assistant that can help the user get the weather', model, }) // Include multiple `function`s as part of the prompt .function( 'getUserLocation', 'gets the location of the user', // This function doesn't need any parameters, // so we do not need to provide a schema async () => { const locations = ['Seattle', 'San Francisco', 'New York']; const randomIndex = Math.floor(Math.random() * locations.length); const location = locations[randomIndex]; log.info('Found user location', location); return location; } ) .function( 'weatherSearch', 'search for weather', { type: 'object', properties: { location: { type: 'string', description: 'the name of the location', }, }, required: ['location'], }, async ({ location }: { location: string }) => { const weatherByLocation: Record = { Seattle: { temperature: 65, condition: 'sunny' }, 'San Francisco': { temperature: 60, condition: 'foggy' }, 'New York': { temperature: 75, condition: 'rainy' }, }; const weather = weatherByLocation[location]; if (!weather) { return 'Sorry, I could not find the weather for that location'; } log.info('Found weather', weather); return weather; } ); // The LLM will then produce a final response to be sent back to the user const result = await prompt.send(activity.text); await send(result.content ?? 'Sorry I could not figure it out'); ``` ## Stopping Functions early You'll notice that after the function responds, `ChatPrompt` re-sends the response from the function invocation back to the LLM which responds back with the user-facing message. It's possible to prevent this "automatic" function calling by passing in a flag ```typescript import { ChatPrompt, IChatModel, Message } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity } from '@microsoft/teams.api'; // ... const result = await prompt.send(activity.text, { autoFunctionCalling: false, // Disable automatic function calling }); // Extract the function call arguments from the result const functionCallArgs = result.function_calls?.[0].arguments; const firstCall = result.function_calls?.[0]; const fnResult = actualFunction(firstCall.arguments); messages.push({ role: 'function', function_id: firstCall.id, content: fnResult, }); // Optionally, you can call the chat prompt again after updating the messages with the results const result = await prompt.send('What should we do next?', { messages, autoFunctionCalling: true, // You can enable it here if you want }); const functionCallArgs = result.function_calls?.[0].arguments; // Extract the function call arguments await send( `The LLM responed with the following structured output: ${JSON.stringify(functionCallArgs, undefined, 2)}.` ); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/keeping-state/csharp.incl.md ================================================ ```csharp using Microsoft.Teams.AI; using Microsoft.Teams.AI.Messages; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; // Simple in-memory store for conversation histories // In your application, it may be a good idea to use a more // persistent store backed by a database or other storage solution private static readonly Dictionary> ConversationStore = new(); /// /// Get or create conversation memory for a specific conversation /// public static List GetOrCreateConversationMemory(string conversationId) { if (!ConversationStore.ContainsKey(conversationId)) { ConversationStore[conversationId] = new List(); } return ConversationStore[conversationId]; } /// /// Clear memory for a specific conversation /// public static Task ClearConversationMemory(string conversationId) { if (ConversationStore.TryGetValue(conversationId, out var messages)) { var messageCount = messages.Count; messages.Clear(); } return Task.CompletedTask; } ``` ```csharp /// /// Example of stateful conversation handler that maintains conversation history /// public static async Task HandleStatefulConversation(OpenAIChatModel model, IContext context) { // Retrieve existing conversation memory or initialize new one var messages = GetOrCreateConversationMemory(context.Activity.Conversation.Id); // Create prompt with conversation-specific memory var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that remembers our previous conversation.") }); // Send with existing messages as context var options = new IChatPrompt.RequestOptions { Messages = messages }; var result = await prompt.Send(context.Activity.Text, options); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); // Update conversation history messages.Add(UserMessage.Text(context.Activity.Text)); messages.Add(new ModelMessage(result.Content)); } else { await context.Reply("I did not generate a response."); } } ``` ### Usage in your application ```csharp teamsApp.OnMessage(async (context, cancellationToken) => { await HandleStatefulConversation(aiModel, context); }); ``` #### How It Works 1. **Conversation Store**: A dictionary maps conversation IDs to their message histories 2. **Per-Conversation Memory**: Each conversation gets its own isolated message list 3. **Request Options**: Pass the message history via `RequestOptions.Messages` when calling `Send()` 4. **Automatic Updates**: After receiving a response, manually add both the user message and AI response to the store 5. **Persistence**: The conversation history persists across multiple user interactions within the same conversation :::tip The `ChatPrompt.Send()` method does **not** automatically update the messages you pass in via `RequestOptions`. You must manually add the user message and AI response to your conversation store after each interaction. ::: :::note In a production application, consider using a more robust storage solution like Azure Cosmos DB, SQL Server, or Redis instead of an in-memory dictionary. This ensures conversation history persists across application restarts and scales across multiple instances. ::: ![Stateful Chat Example](/screenshots/stateful-chat-example.png) ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/keeping-state/python.incl.md ================================================ ```python from microsoft_teams.ai import ChatPrompt, ListMemory, AIModel from microsoft_teams.openai import OpenAICompletionsAIModel # Simple in-memory store for conversation histories # In your application, it may be a good idea to use a more # persistent store backed by a database or other storage solution conversation_store: dict[str, ListMemory] = {} # Initialize AI model ai_model = OpenAICompletionsAIModel(model="gpt-4") def get_or_create_conversation_memory(conversation_id: str) -> ListMemory: """Get or create conversation memory for a specific conversation""" if conversation_id not in conversation_store: conversation_store[conversation_id] = ListMemory() return conversation_store[conversation_id] async def clear_conversation_memory(conversation_id: str) -> None: """Clear memory for a specific conversation""" if conversation_id in conversation_store: memory = conversation_store[conversation_id] await memory.set_all([]) print(f"Cleared memory for conversation {conversation_id}") ``` ```python from microsoft_teams.ai import ChatPrompt, ListMemory, AIModel from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext # ... async def handle_stateful_conversation(model: AIModel, ctx: ActivityContext[MessageActivity]) -> None: """Example of stateful conversation handler that maintains conversation history""" print(f"Received message: {ctx.activity.text}") # Retrieve existing conversation memory or initialize new one memory = get_or_create_conversation_memory(ctx.activity.conversation.id) # Get existing messages for logging existing_messages = await memory.get_all() print(f"Existing messages before sending to prompt: {len(existing_messages)} messages") # Create ChatPrompt with conversation-specific memory chat_prompt = ChatPrompt(model, memory=memory) chat_result = await chat_prompt.send( input=ctx.activity.text, instructions="You are a helpful assistant that remembers our previous conversation." ) if chat_result.response.content: message = MessageActivityInput(text=chat_result.response.content).add_ai_generated() await ctx.send(message) else: await ctx.reply("I did not generate a response.") # Log final message count final_messages = await memory.get_all() print(f"Messages after sending to prompt: {len(final_messages)} messages") @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle messages using stateful conversation""" await handle_stateful_conversation(ai_model, ctx) ``` ![Screenshot of chat between user and agent, user first states 'My dinosaur's name is Barnie' and later asks What's my pet's name and the agent responds correctly with 'Barnie'.](/screenshots/stateful-chat-example.png) ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/keeping-state/typescript.incl.md ================================================ ```typescript import { ChatPrompt, IChatModel, Message } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity, MessageActivity } from '@microsoft/teams.api'; // ... // Simple in-memory store for conversation histories // In your application, it may be a good idea to use a more // persistent store backed by a database or other storage solution const conversationStore = new Map(); const getOrCreateConversationHistory = (conversationId: string) => { // Check if conversation history exists const existingMessages = conversationStore.get(conversationId); if (existingMessages) { return existingMessages; } // If not, create a new conversation history const newMessages: Message[] = []; conversationStore.set(conversationId, newMessages); return newMessages; }; ``` ```typescript /** * Example of a stateful conversation handler that maintains conversation history * using an in-memory store keyed by conversation ID. * @param model The chat model to use * @param activity The incoming activity * @param send Function to send an activity */ export const handleStatefulConversation = async ( model: IChatModel, activity: IMessageActivity, send: (activity: ActivityLike) => Promise, log: ILogger ) => { log.info('Received message', activity.text); // Retrieve existing conversation history or initialize new one const existingMessages = getOrCreateConversationHistory(activity.conversation.id); log.info('Existing messages before sending to prompt', existingMessages); // Create prompt with existing messages const prompt = new ChatPrompt({ instructions: 'You are a helpful assistant.', model, messages: existingMessages, // Pass in existing conversation history }); const result = await prompt.send(activity.text); if (result) { await send( result.content != null ? new MessageActivity(result.content).addAiGenerated() : 'I did not generate a response.' ); } log.info('Messages after sending to prompt:', existingMessages); }; ``` ![Stateful Chat Example](/screenshots/stateful-chat-example.png) ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-client/csharp.incl.md ================================================ SSE protocol Install it to your application: ```bash dotnet add package Microsoft.Teams.Plugins.External.McpClient --prerelease ``` SSE a valid SSE and keys `MCPClientPlugin` (from `Microsoft.Teams.Plugins.External.McpClient` package) object as a plugin `send` import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Plugins.AspNetCore.Extensions; using Microsoft.Teams.Plugins.External.McpClient; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.AddTeams(); WebApplication webApp = builder.Build(); OpenAIChatPrompt prompt = new( new OpenAIChatModel( model: "gpt-4o", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!), new ChatPromptOptions() .WithDescription("helpful assistant") .WithInstructions( "You are a helpful assistant that can help answer questions using Microsoft docs.", "You MUST use tool calls to do all your work.") ); prompt.Plugin(new McpClientPlugin().UseMcpServer("https://learn.microsoft.com/api/mcp")); App app = webApp.UseTeams(); app.OnMessage(async (context, cancellationToken) => { await context.Send(new TypingActivity(), cancellationToken); var result = await prompt.Send(context.Activity.Text); await context.Send(result.Content, cancellationToken); }); webApp.Run(); ``` In this example, we augment the `ChatPrompt` with a remote MCP Server. :::note You can quickly set up an MCP server using [Azure Functions](https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059). ::: ### Custom Headers Some MCP servers may require custom headers to be sent as part of the request. You can customize the headers when calling the `UseMcpServer` function: ```csharp new McpClientPlugin() .UseMcpServer("https://learn.microsoft.com/api/mcp", new McpClientPluginParams() { HeadersFactory = () => new Dictionary() { { "HEADER_KEY", "HEADER_VALUE" } } } ); ``` ![Animated image of user typing a prompt ('Tell me about Charizard') to DevTools Chat window and multiple paragraphs of information being returned.](/screenshots/mcp-client-pokemon.gif) In this example, our MCP server is a Pokemon API and our client knows how to call it. The LLM is able to call the `getPokemon` function exposed by the server and return the result back to the user. ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-client/python.incl.md ================================================ SSE protocol Install it to your application: ```bash pip install microsoft-teams-mcpplugin ``` StreamableHttp/SSE a valid SSE any keys `McpClientPlugin` as a plugin `send` ```python from microsoft_teams.ai import ChatPrompt from microsoft_teams.mcpplugin import McpClientPlugin from microsoft_teams.openai import OpenAICompletionsAIModel # ... # Set up AI model completions_model = OpenAICompletionsAIModel(model="gpt-4") # Configure MCP Client Plugin with multiple remote servers mcp_plugin = McpClientPlugin() # Add multiple MCP servers mcp_plugin.use_mcp_server("https://learn.microsoft.com/api/mcp") mcp_plugin.use_mcp_server("https://example.com/mcp/weather") mcp_plugin.use_mcp_server("https://example.com/mcp/pokemon") # ChatPrompt with MCP tools chat_prompt = ChatPrompt( completions_model, plugins=[mcp_plugin] ) ``` In this example, we augment the `ChatPrompt` with multiple remote MCP Servers. ## Using MCP Client in Message Handlers ```python from microsoft_teams.ai import ChatPrompt from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext # ... @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle messages using ChatPrompt with MCP tools""" result = await chat_prompt.send( input=ctx.activity.text, instructions="You are a helpful assistant with access to remote MCP tools." ) if result.response.content: message = MessageActivityInput(text=result.response.content).add_ai_generated() await ctx.send(message) ``` :::note Feel free to build an MCP Server in a different agent using the [MCP Server Guide](./mcp-server). Or you can quickly set up an MCP server using [Azure Functions](https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059). ::: ### Customize Headers Some MCP servers may require custom headers to be sent as part of the request. You can customize the headers when calling the `use_mcp_server` function: ```python from os import getenv from microsoft_teams.mcpplugin import McpClientPlugin, McpClientPluginParams # ... # Example with Bearer token authentication GITHUB_PAT = getenv("GITHUB_PAT") if GITHUB_PAT: mcp_plugin.use_mcp_server( "https://api.githubcopilot.com/mcp/", McpClientPluginParams(headers={ "Authorization": f"Bearer {GITHUB_PAT}", }) ) # Example with API key mcp_plugin.use_mcp_server( "https://example.com/api/mcp", McpClientPluginParams(headers={ "X-API-Key": getenv('API_KEY'), "Custom-Header": "custom-value" }) ) ``` ![Animated image of user typing a prompt ('Tell me about Charizard') to DevTools Chat window and multiple paragraphs of information being returned.](/screenshots/mcp-client-pokemon.gif) In this example, our MCP server is a Pokemon API and our client knows how to call it. The LLM is able to call the `getPokemon` function exposed by the server and return the result back to the user. ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-client/typescript.incl.md ================================================ Streamable HTTP protocol Install it to your application: ```bash npm install @microsoft/teams.mcpclient ``` Streamable HTTP/SSE valid remote and keys `MCPClientPlugin` (from `@microsoft/teams.mcpclient` package) object as a plugin `send` ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; import { OpenAIChatModel } from '@microsoft/teams.openai'; // ... const logger = new ConsoleLogger('mcp-client', { level: 'debug' }); const prompt = new ChatPrompt( { instructions: 'You are a helpful assistant. You MUST use tool calls to do all your work.', model: new OpenAIChatModel({ model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY, }), }, [new McpClientPlugin({ logger })] ).usePlugin('mcpClient', { url: 'https://learn.microsoft.com/api/mcp', }); const app = new App(); app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); const result = await prompt.send(activity.text); if (result.content) { await send(result.content); } }); app.start().catch(console.error); ``` In this example, we augment the `ChatPrompt` with a few remote MCP Servers. ### Customize Headers Some MCP servers may require custom headers to be sent as part of the request. You can customize the headers when calling the `usePlugin` function: ```typescript import { ChatPrompt } from '@microsoft/teams.ai'; import { McpClientPlugin } from '@microsoft/teams.mcpclient'; // ... .usePlugin('mcpClient', { url: 'https:///mcp' params: { headers: { 'x-header-functions-key': '', } } }); ``` :::note Feel free to build an MCP Server in a different agent using the [MCP Server Guide](./mcp-server). Or you can quickly set up an MCP server using [Azure Functions](https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059). ::: ![Animated image of user typing a prompt ('Tell me about Charizard') to DevTools Chat window and multiple paragraphs of information being returned.](/screenshots/mcp-client-pokemon.gif) In this example, our MCP server is a Pokemon API and our client knows how to call it. The LLM is able to call the `getPokemon` function exposed by the server and return the result back to the user. ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-server/csharp.incl.md ================================================ WIP ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-server/python.incl.md ================================================ You are able to convert any `App` into an MCP server by using the `McpPlugin` from the `microsoft-teams-mcp` package. This plugin adds the necessary endpoints to your application to serve as an MCP server. The plugin allows you to define tools, resources, and prompts that can be exposed to other MCP applications. Install it to your application: ```bash pip install microsoft-teams-mcpplugin ``` Your plugin can be configured as follows: ```python from microsoft_teams.ai import Function from microsoft_teams.mcpplugin import McpServerPlugin from pydantic import BaseModel # ... # Configure MCP server with custom name mcp_server_plugin = McpServerPlugin( name="test-mcp", ) class EchoParams(BaseModel): input: str async def echo_handler(params: EchoParams) -> str: return f"You said {params.input}" # Register the echo tool mcp_server_plugin.use_tool( Function( name="echo", description="echo back whatever you said", parameter_schema=EchoParams, handler=echo_handler, ) ) ``` :::note By default, the MCP server will be available at `/mcp` on your application. You can change this by setting the `path` property in the plugin configuration. ::: And included in the app like any other plugin: ```python from microsoft_teams.apps import App from microsoft_teams.devtools import DevToolsPlugin # ... app = App(plugins=[mcp_server_plugin, DevToolsPlugin()]) ``` :::tip You may use the [MCP-Inspector](https://modelcontextprotocol.io/legacy/tools/inspector) to test functionality with your server. ::: ![MCP Server in Devtools](/screenshots/mcp-inspector.gif) **Alert Tool for Proactive Messaging:** ```python from typing import Dict from microsoft_teams.ai import Function from microsoft_teams.mcpplugin import McpServerPlugin from pydantic import BaseModel # ... # Storage for conversation IDs (for proactive messaging) conversation_storage: Dict[str, str] = {} class AlertParams(BaseModel): user_id: str message: str async def alert_handler(params: AlertParams) -> str: """ Send proactive message to user via Teams. This demonstrates the "piping messages to user" feature. """ # 1. Validate if the incoming request is allowed to send messages if not params.user_id or not params.message: return "Invalid parameters: user_id and message are required" # 2. Fetch the correct conversation ID for the given user conversation_id = conversation_storage.get(params.user_id) if not conversation_id: return f"No conversation found for user {params.user_id}. User needs to message the bot first." # 3. Send proactive message to the user await app.send(conversation_id=conversation_id, activity=params.message) return f"Alert sent to user {params.user_id}: {params.message}" # Register the alert tool mcp_server_plugin.use_tool( Function( name="alert", description="Send proactive message to a Teams user", parameter_schema=AlertParams, handler=alert_handler, ) ) ``` **Store Conversation IDs in Message Handler:** ```python from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext # ... @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """ Handle incoming messages and store conversation IDs for proactive messaging. """ # Store conversation ID for this user (for proactive messaging) user_id = ctx.activity.from_.id conversation_id = ctx.activity.conversation.id conversation_storage[user_id] = conversation_id # Echo back the message with info about stored conversation await ctx.reply( f"You said: {ctx.activity.text}\n\n" f"📝 Stored conversation ID `{conversation_id}` for user `{user_id}` " f"(for proactive messaging via MCP alert tool)" ) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/mcp/mcp-server/typescript.incl.md ================================================ You are able to convert any `App` into an MCP server by using the `McpPlugin`. This plugin adds the necessary endpoints to your application to serve as an MCP server. The plugin allows you to define tools, resources, and prompts that can be exposed to other MCP applications. Install it to your application: ```bash npm install @microsoft/teams.mcp ``` Your plugin can be configured as follows: ```typescript import { z } from 'zod'; import { App } from '@microsoft/teams.apps'; import { McpPlugin } from '@microsoft/teams.mcp'; // ... const mcpServerPlugin = new McpPlugin({ // Describe the MCP server with a helpful name and description // for MCP clients to discover and use it. name: 'test-mcp', description: 'Allows you to test the mcp server', // Optionally, you can provide a URL to the mcp dev-tools // during development inspector: 'http://localhost:5173?proxyPort=9000', }).tool( // Describe the tools with helpful names and descriptions 'echo', 'echos back whatever you said', { input: z.string().describe('the text to echo back'), }, { readOnlyHint: true, idempotentHint: true, }, async ({ input }) => { return { content: [ { type: 'text', text: `you said "${input}"`, }, ], }; } ); ``` :::note By default, the MCP server will be available at `/mcp` on your application. You can change this by setting the `transport.path` property in the plugin configuration. ::: And included in the app like any other plugin: ```typescript import { App } from '@microsoft/teams.apps'; import { DevtoolsPlugin } from '@microsoft/teams.dev'; import { McpPlugin } from '@microsoft/teams.mcp'; // ... const app = new App({ plugins: [ new DevtoolsPlugin(), // Add this plugin mcpServerPlugin, ], }); ``` :::tip Enabling mcp request inspection and the `DevtoolsPlugin` allows you to see all the requests and responses to and from your MCP server (similar to how the **Activities** tab works). ::: ![MCP Server in Devtools](/screenshots/mcp-devtools.gif) ```typescript import { z } from 'zod'; import { App } from '@microsoft/teams.apps'; import { McpPlugin } from '@microsoft/teams.mcp'; // ... // Keep a store of the user to the conversation id // In a production app, you probably would want to use a // persistent store like a database const userToConversationId = new Map(); // Add a an MCP server tool mcpServerPlugin.tool( 'alertUser', 'alerts the user about something important', { input: z.string().describe('the text to echo back'), userAadObjectId: z.string().describe('the user to alert'), }, { readOnlyHint: true, idempotentHint: true, }, async ({ input, userAadObjectId }, { authInfo }) => { if (!isAuthValid(authInfo)) { throw new Error('Not allowed to call this tool'); } const conversationId = userToConversationId.get(userAadObjectId); if (!conversationId) { console.log('Current conversation map', userToConversationId); return { content: [ { type: 'text', text: `user ${userAadObjectId} is not in a conversation`, }, ], }; } // Leverage the app's proactive messaging capabilities to send a mesage to // correct conversation id. await app.send(conversationId, `Notification: ${input}`); return { content: [ { type: 'text', text: 'User was notified', }, ], }; } ); ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('message', async ({ send, activity }) => { await send({ type: 'typing' }); await send(`you said "${activity.text}"`); if (activity.from.aadObjectId && !userToConversationId.has(activity.from.aadObjectId)) { userToConversationId.set(activity.from.aadObjectId, activity.conversation.id); app.log.info( `Just added user ${activity.from.aadObjectId} to conversation ${activity.conversation.id}` ); } }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/python.incl.md ================================================ `microsoft-teams-ai` package ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/csharp.incl.md ================================================ **NuGet Package** - Install the Microsoft Teams SDK: ```bash dotnet add package Microsoft.Teams.AI ``` You should include your keys securely using `appsettings.json` or environment variables N/A Once you have deployed a model, configure your application using `appsettings.json` or `appsettings.Development.json`: **appsettings.Development.json** ```json { "AzureOpenAIKey": "your-azure-openai-api-key", "AzureOpenAIModel": "your-azure-openai-model-deployment-name", "AzureOpenAIEndpoint": "https://your-resource.openai.azure.com/" } ``` **Using configuration in your code:** ```csharp var azureOpenAIModel = configuration["AzureOpenAIModel"] ?? throw new InvalidOperationException("AzureOpenAIModel not configured"); var azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"] ?? throw new InvalidOperationException("AzureOpenAIEndpoint not configured"); var azureOpenAIKey = configuration["AzureOpenAIKey"] ?? throw new InvalidOperationException("AzureOpenAIKey not configured"); var azureOpenAI = new AzureOpenAIClient( new Uri(azureOpenAIEndpoint), new ApiKeyCredential(azureOpenAIKey) ); var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); ``` :::tip Use `appsettings.Development.json` for local development and keep it in `.gitignore`. For production, use environment variables or Azure Key Vault. ::: :::info The Azure OpenAI SDK handles API versioning automatically. You don't need to specify an API version manually. ::: Once you have your API key, configure your application: **appsettings.Development.json** ```json { "OpenAIKey": "sk-your-openai-api-key", "OpenAIModel": "gpt-4o" } ``` **Using configuration in your code:** ```csharp var openAIKey = configuration["OpenAIKey"] ?? throw new InvalidOperationException("OpenAIKey not configured"); var openAIModel = configuration["OpenAIModel"] ?? "gpt-4o"; var aiModel = new OpenAIChatModel(openAIModel, openAIKey); ``` :::tip Use `appsettings.Development.json` for local development and keep it in `.gitignore`. For production, use environment variables or Azure Key Vault. ::: N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/python.incl.md ================================================ N/A We recommend putting it in an .env file at the root level of your project ``` my-app/ |── appPackage/ # Teams app package files ├── src/ │ └── main.py # Main application code |── .env # Environment variables ``` Once you have deployed a model, include the following key/values in your `.env` file: ```env AZURE_OPENAI_API_KEY=your-azure-openai-api-key AZURE_OPENAI_MODEL=your-azure-openai-model-deployment-name AZURE_OPENAI_ENDPOINT=your-azure-openai-endpoint AZURE_OPENAI_API_VERSION=your-azure-openai-api-version ``` :::info The `AZURE_OPENAI_API_VERSION` is different from the model version. This is a common point of confusion. Look for the API Version [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference?WT.mc_id=AZ-MVP-5004796 'Azure OpenAI API Reference') ::: Once you have your API key, include the following key/values in your `.env` file: ```env OPENAI_API_KEY=sk-your-openai-api-key OPENAI_MODEL=gpt-4 # Optional: defaults to gpt-4o if not specified ``` :::note **Automatic Environment Variable Loading**: The AI models automatically read these environment variables when initialized. You can also pass these values explicitly as constructor parameters if needed for advanced configurations. ```python # Automatic (recommended) model = OpenAICompletionsAIModel(model="your-model-name") # Explicit (for advanced use cases) model = OpenAICompletionsAIModel( key="your-api-key", model="your-model-name", azure_endpoint="your-endpoint", # Azure only api_version="your-api-version" # Azure only ) ``` ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/typescript.incl.md ================================================ Install the required AI packages to your application: ```bash npm install @microsoft/teams.apps @microsoft/teams.ai @microsoft/teams.openai ``` For development, you may also want to install the DevTools plugin: ```bash npm install @microsoft/teams.dev --save-dev ``` We recommend putting it in an .env file at the root level of your project ``` my-app/ |── appPackage/ # Teams app package files ├── src/ │ └── index.ts # Main application code |── .env # Environment variables ``` Once you have deployed a model, include the following key/values in your `.env` file: ```env AZURE_OPENAI_API_KEY=your-azure-openai-api-key AZURE_OPENAI_MODEL_DEPLOYMENT_NAME=your-azure-openai-model AZURE_OPENAI_ENDPOINT=your-azure-openai-endpoint AZURE_OPENAI_API_VERSION=your-azure-openai-api-version ``` :::info The `AZURE_OPENAI_API_VERSION` is different from the model version. This is a common point of confusion. Look for the API Version [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference?WT.mc_id=AZ-MVP-5004796 'Azure OpenAI API Reference') ::: Once you have your API key, include the following key/values in your `.env` file: ```env OPENAI_API_KEY=sk-your-openai-api-key ``` :::note **Automatic Environment Variable Loading**: The OpenAI model automatically reads environment variables when options are not explicitly provided. You can pass values explicitly as constructor parameters if needed for advanced configurations. ```typescript // Automatic (recommended) - uses environment variables const model = new OpenAIChatModel({ model: 'gpt-4o', }); // Explicit (for advanced use cases) const model = new OpenAIChatModel({ apiKey: 'your-api-key', model: 'gpt-4o', endpoint: 'your-endpoint', // Azure only apiVersion: 'your-api-version', // Azure only baseUrl: 'your-base-url', // Custom base URL organization: 'your-org-id', // Optional project: 'your-project-id', // Optional }); ``` **Environment variables automatically loaded:** - `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` - `AZURE_OPENAI_ENDPOINT` (Azure only) - `OPENAI_API_VERSION` (Azure only) ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/ai/typescript.incl.md ================================================ `@microsoft/teams.ai` package ================================================ FILE: teams.md/src/components/include/in-depth-guides/csharp.incl.md ================================================ This documentation covers advanced features and capabilities of the Teams SDK in C#. ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/creating-dialogs/csharp.incl.md ================================================ To open a dialog, you need to supply a special type of action to the Adaptive Card. The `TaskFetchAction` is specifically designed for this purpose - it automatically sets up the proper Teams data structure to trigger a dialog. Once this button is clicked, the dialog will open and ask the application what to show. ```csharp using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; using Microsoft.Teams.Common.Logging; //... [Message] public async Task OnMessage([Context] MessageActivity activity, [Context] IContext.Client client, [Context] ILogger log) { // Create the launcher adaptive card var card = CreateDialogLauncherCard(); await client.Send(card); } private static AdaptiveCard CreateDialogLauncherCard() { var card = new AdaptiveCard { Body = new List { new TextBlock("Select the examples you want to see!") { Size = TextSize.Large, Weight = TextWeight.Bolder } }, Actions = new List { new TaskFetchAction(new { opendialogtype = "simple_form" }) { Title = "Simple form test" }, new TaskFetchAction(new { opendialogtype = "webpage_dialog" }) { Title = "Webpage Dialog" }, new TaskFetchAction(new { opendialogtype = "multi_step_form" }) { Title = "Multi-step Form" } } }; return card; } ``` Once an action is executed to open a dialog, the Teams client will send an event to the agent to request what the content of the dialog should be. When using `TaskFetchAction`, the data is nested inside an `MsTeams` property structure. ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskFetch] public Microsoft.Teams.Api.TaskModules.Response OnTaskFetch([Context] Tasks.FetchActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_FETCH] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var dialogType = data.Value.TryGetProperty("opendialogtype", out var dialogTypeElement) && dialogTypeElement.ValueKind == JsonValueKind.String ? dialogTypeElement.GetString() : null; log.Info($"[TASK_FETCH] Dialog type: {dialogType}"); return dialogType switch { "simple_form" => CreateSimpleFormDialog(), "webpage_dialog" => CreateWebpageDialog(_configuration, log), "multi_step_form" => CreateMultiStepFormDialog(), "mixed_example" => CreateMixedExampleDialog(), _ => new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown dialog type")) }; } ``` ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... private static Microsoft.Teams.Api.TaskModules.Response CreateSimpleFormDialog() { var choices = new List { new Choice { Title = "Option 1", Value = "opt1" }, new Choice { Title = "Option 2", Value = "opt2" }, new Choice { Title = "Option 3", Value = "opt3" } }; var dialogCard = new AdaptiveCard { Body = new List { new TextBlock("This is a simple form") { Size = TextSize.Large, Weight = TextWeight.Bolder }, new TextInput { Id = "name", Label = "Name", Placeholder = "Enter your name", IsRequired = true }, new ChoiceSetInput { Id = "preference", Label = "Select your preference", Choices = choices, Style = StyleEnum.Compact } }, Actions = new List { new SubmitAction { Title = "Submit", Data = new { submissiondialogtype = "simple_form" } } } }; var taskInfo = new TaskInfo { Title = "Simple Form Dialog", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = dialogCard } }; return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo)); } ``` ```csharp using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Common; //... private static Microsoft.Teams.Api.TaskModules.Response CreateWebpageDialog(IConfiguration configuration, ILogger log) { var botEndpoint = configuration["BotEndpoint"]; if (string.IsNullOrEmpty(botEndpoint)) { log.Warn("No remote endpoint detected. Using webpages for dialog will not work as expected"); botEndpoint = "http://localhost:3978"; // Fallback for local development } else { log.Info($"Using BotEndpoint: {botEndpoint}/tabs/dialog-form"); } var taskInfo = new TaskInfo { Title = "Webpage Dialog", Width = new Union(1000), Height = new Union(800), // Here we are using a webpage that is hosted in the same // server as the agent. This server needs to be publicly accessible, // needs to set up teams.js client library (https://www.npmjs.com/package/@microsoft/teams-js) // and needs to be registered in the manifest. Url = $"{botEndpoint}/tabs/dialog-form" }; return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo)); } ``` ### Setting up Embedded Web Content To serve web content for dialogs, you can use the `AddTab` functionality to embed HTML files as resources: ```csharp // In Program.cs when building your app app.UseTeams(); app.AddTab("dialog-form", "Web/dialog-form"); // Configure project file to embed web resources // In .csproj: // true // // ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/creating-dialogs/python.incl.md ================================================ To open a dialog, you need to supply a special type of action as to the Adaptive Card. Once this button is clicked, the dialog will open and ask the application what to show. ```python from microsoft_teams.api import MessageActivity, MessageActivityInput, TypingActivityInput from microsoft_teams.apps import ActivityContext from microsoft_teams.cards import AdaptiveCard, TextBlock, TaskFetchAction # ... @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(TypingActivityInput()) card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextBlock( text="Select the examples you want to see!", size="Large", weight="Bolder", ) ] ).with_actions([ # Special type of action to open a dialog TaskFetchAction(value={"OpenDialogType": "webpage_dialog"}).with_title("Webpage Dialog"), # This data will be passed back in an event, so we can handle what to show in the dialog TaskFetchAction(value={"OpenDialogType": "multi_step_form"}).with_title("Multi-step Form"), TaskFetchAction(value={"OpenDialogType": "mixed_example"}).with_title("Mixed Example") ]) # Send the card as an attachment message = MessageActivityInput(text="Enter this form").add_card(card) await ctx.send(message) ``` Once an action is executed to open a dialog, the Teams client will send an event to the agent to request what the content of the dialog should be. Here is how to handle this event: ```python @app.on_dialog_open async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): """Handle dialog open events for all dialog types.""" card = AdaptiveCard(...) # Return an object with the task value that renders a card return InvokeResponse( body=TaskModuleResponse( task=TaskModuleContinueResponse( value=CardTaskModuleTaskInfo( title="Title of Dialog", card=card_attachment(AdaptiveCardAttachment(content=card)), ) ) ) ) ``` ```python from microsoft_teams.api import AdaptiveCardAttachment, TaskFetchInvokeActivity, InvokeResponse, card_attachment from microsoft_teams.api import CardTaskModuleTaskInfo, TaskModuleContinueResponse, TaskModuleResponse from microsoft_teams.apps import ActivityContext from microsoft_teams.cards import AdaptiveCard, TextBlock, TextInput, SubmitAction, SubmitActionData # ... @app.on_dialog_open async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): """Handle dialog open events for all dialog types.""" # Return an object with the task value that renders a card dialog_card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextBlock(text="This is a simple form", size="Large", weight="Bolder"), TextInput().with_label("Name").with_is_required(True).with_id("name").with_placeholder("Enter your name"), ], actions=[ SubmitAction().with_title("Submit").with_data(SubmitActionData(ms_teams={"SubmissionDialogType": "simple_form"})) ] ) # Return an object with the task value that renders a card return InvokeResponse( body=TaskModuleResponse( task=TaskModuleContinueResponse( value=CardTaskModuleTaskInfo( title="Simple Form Dialog", card=card_attachment(AdaptiveCardAttachment(content=dialog_card)), ) ) ) ) ``` ```python import os from microsoft_teams.api import InvokeResponse, TaskModuleContinueResponse, TaskModuleResponse, UrlTaskModuleTaskInfo # ... return InvokeResponse( body=TaskModuleResponse( task=TaskModuleContinueResponse( value=UrlTaskModuleTaskInfo( title="Webpage Dialog", # Here we are using a webpage that is hosted in the same # server as the agent. This server needs to be publicly accessible, # needs to set up teams.js client library (https://www.npmjs.com/package/@microsoft/teams-js) # and needs to be registered in the manifest. url=f"{os.getenv('BOT_ENDPOINT')}/tabs/dialog-webpage", width=1000, height=800, ) ) ) ) ``` ### Setting up Embedded Web Content To serve web content for dialogs, you can use the `page` method to host static webpages: ```python import os # In your app setup (e.g., main.py) # Hosts a static webpage at /tabs/dialog-form app.page("customform", os.path.join(os.path.dirname(__file__), "views", "customform"), "/tabs/dialog-form") ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/creating-dialogs/typescript.incl.md ================================================ To open a dialog, you need to supply a special type of action as to the Adaptive Card. Once this button is clicked, the dialog will open and ask the application what to show. ```typescript import { cardAttachment, MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, IAdaptiveCard, TaskFetchAction, TaskFetchData, } from '@microsoft/teams.cards'; // ... app.on('message', async ({ send }) => { await send({ type: 'typing' }); // Create the launcher adaptive card const card: IAdaptiveCard = new AdaptiveCard({ type: 'TextBlock', text: 'Select the examples you want to see!', size: 'Large', weight: 'Bolder', }).withActions( // raw action { type: 'Action.Submit', title: 'Simple form test', data: { msteams: { type: 'task/fetch', }, opendialogtype: 'simple_form', }, }, // Special type of action to open a dialog new TaskFetchAction({}) .withTitle('Webpage Dialog') // This data will be passed back in an event so we can // handle what to show in the dialog .withValue(new TaskFetchData({ opendialogtype: 'webpage_dialog' })), new TaskFetchAction({}) .withTitle('Multi-step Form') .withValue(new TaskFetchData({ opendialogtype: 'multi_step_form' })), new TaskFetchAction({}) .withTitle('Mixed Example') .withValue(new TaskFetchData({ opendialogtype: 'mixed_example' })) ); // Send the card as an attachment await send(new MessageActivity('Enter this form').addCard('adaptive', card)); }); ``` Once an action is executed to open a dialog, the Teams client will send an event to the agent to request what the content of the dialog should be. Here is how to handle this event: ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, IAdaptiveCard } from '@microsoft/teams.cards'; // ... app.on('dialog.open', async ({ activity }) => { const card: IAdaptiveCard = new AdaptiveCard()... // Return an object with the task value that renders a card return { task: { type: 'continue', value: { title: 'Title of Dialog', card: cardAttachment('adaptive', card), }, }, }; } ``` ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { AdaptiveCard, TextInput, SubmitAction } from '@microsoft/teams.cards'; // ... if (dialogType === 'simple_form') { const dialogCard = new AdaptiveCard( { type: 'TextBlock', text: 'This is a simple form', size: 'Large', weight: 'Bolder', }, new TextInput() .withLabel('Name') .withIsRequired() .withId('name') .withPlaceholder('Enter your name') ) // Inside the dialog, the card actions for submitting the card must be // of type Action.Submit .withActions( new SubmitAction().withTitle('Submit').withData({ submissiondialogtype: 'simple_form' }) ); // Return an object with the task value that renders a card return { task: { type: 'continue', value: { title: 'Simple Form Dialog', card: cardAttachment('adaptive', dialogCard), }, }, }; } ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... return { task: { type: 'continue', value: { title: 'Webpage Dialog', // Here we are using a webpage that is hosted in the same // server as the agent. This server needs to be publicly accessible, // needs to set up teams.js client library (https://www.npmjs.com/package/@microsoft/teams-js) // and needs to be registered in the manifest. url: `${process.env['BOT_ENDPOINT']}/tabs/dialog-form`, width: 1000, height: 800, }, }, }; ``` ### Setting up Embedded Web Content To serve web content for dialogs, you can use the `tab` method to host static webpages: ```typescript import path from 'path'; // In your app setup (e.g., index.ts) // Hosts a static webpage at /tabs/dialog-form app.tab('dialog-form', path.join(__dirname, 'views', 'customform')); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-dialog-submissions/csharp.incl.md ================================================ Dialogs have a specific `TaskSubmit` event to handle submissions. When a user submits a form inside a dialog, the app is notified via this event, which is then handled to process the submission values, and can either send a response or proceed to more steps in the dialogs (see [Multi-step Dialogs](./handling-multi-step-forms)). :::warning Return Type Requirement Methods decorated with `[TaskSubmit]` **must** return `Task`. Every code path must return a Response object containing either a `MessageTask` (to show a message and close the dialog) or a `ContinueTask` (to show another dialog). Using just `Task` or `void` will compile but fail at runtime when the Teams client expects a Response object. ::: ## Basic Example ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "simple_form": var name = GetFormValue("name") ?? "Unknown"; await client.Send($"Hi {name}, thanks for submitting the form!"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form was submitted")); // More examples below default: return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown submission type")); } } ``` ```csharp // Add this case to the switch statement in OnTaskSubmit method case "webpage_dialog": var webName = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; await client.Send($"Hi {webName}, thanks for submitting the form! We got that your email is {email}"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form submitted successfully")); ``` ### Complete TaskSubmit Handler Example Here's the complete example showing how to handle multiple submission types: ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "simple_form": var name = GetFormValue("name") ?? "Unknown"; await client.Send($"Hi {name}, thanks for submitting the form!"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form was submitted")); case "webpage_dialog": var webName = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; await client.Send($"Hi {webName}, thanks for submitting the form! We got that your email is {email}"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form submitted successfully")); default: return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown submission type")); } } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-dialog-submissions/python.incl.md ================================================ Dialogs have a specific `dialog_submit` event to handle submissions. When a user submits a form inside a dialog, the app is notified via this event, which is then handled to process the submission values, and can either send a response or proceed to more steps in the dialogs (see [Multi-step Dialogs](./handling-multi-step-forms)). ```python from typing import Optional, Any from microsoft_teams.api import TaskSubmitInvokeActivity, TaskModuleResponse, TaskModuleMessageResponse from microsoft_teams.apps import ActivityContext # ... @app.on_dialog_submit async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): """Handle dialog submit events for all dialog types.""" data: Optional[Any] = ctx.activity.value.data dialog_type = data.get("submissiondialogtype") if data else None if dialog_type == "simple_form": name = data.get("name") if data else None await ctx.send(f"Hi {name}, thanks for submitting the form!") return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form was submitted")) ``` ```python from typing import Optional, Any from microsoft_teams.api import TaskSubmitInvokeActivity, InvokeResponse, TaskModuleResponse, TaskModuleMessageResponse from microsoft_teams.apps import ActivityContext # ... @app.on_dialog_submit async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): """Handle dialog submit events for all dialog types.""" data: Optional[Any] = ctx.activity.value.data dialog_type = data.get("submissiondialogtype") if data else None if dialog_type == "webpage_dialog": name = data.get("name") if data else None email = data.get("email") if data else None await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") return InvokeResponse( body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted successfully")) ) ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-dialog-submissions/typescript.incl.md ================================================ Dialogs have a specific `dialog.submit` event to handle submissions. When a user submits a form inside a dialog, the app is notified via this event, which is then handled to process the submission values, and can either send a response or proceed to more steps in the dialogs (see [Multi-step Dialogs](./handling-multi-step-forms)). ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('dialog.submit', async ({ activity, send, next }) => { const dialogType = activity.value.data?.submissiondialogtype; if (dialogType === 'simple_form') { // This is data from the form that was submitted const name = activity.value.data.name; await send(`Hi ${name}, thanks for submitting the form!`); return { task: { type: 'message', // This appears as a final message in the dialog value: 'Form was submitted', }, }; } }); ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... // The submission from a webpage happens via the microsoftTeams.tasks.submitTask(formData) // call. app.on('dialog.submit', async ({ activity, send, next }) => { const dialogType = activity.value.data.submissiondialogtype; if (dialogType === 'webpage_dialog') { // This is data from the form that was submitted const name = activity.value.data.name; const email = activity.value.data.email; await send(`Hi ${name}, thanks for submitting the form! We got that your email is ${email}`); // You can also return a blank response return { status: 200, }; } }); ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-multi-step-forms/csharp.incl.md ================================================ ## Creating the Initial Dialog Start off by sending an initial card in the `TaskFetch` event. ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... private static Response CreateMultiStepFormDialog() { var cardJson = """ { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "This is a multi-step form", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "name", "label": "Name", "placeholder": "Enter your name", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_1"} } ] } """; var dialogCard = JsonSerializer.Deserialize(cardJson) ?? throw new InvalidOperationException("Failed to deserialize multi-step form card"); var taskInfo = new TaskInfo { Title = "Multi-step Form Dialog", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = dialogCard } }; return new Response(new ContinueTask(taskInfo)); } ``` Then in the submission handler, you can choose to `continue` the dialog with a different card. ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... // Add these cases to your OnTaskSubmit method case "webpage_dialog_step_1": var nameStep1 = GetFormValue("name") ?? "Unknown"; var nextStepCardJson = $$""" { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "email", "label": "Email", "placeholder": "Enter your email", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": "{{nameStep1}}"} } ] } """; var nextStepCard = JsonSerializer.Deserialize(nextStepCardJson) ?? throw new InvalidOperationException("Failed to deserialize next step card"); var nextStepTaskInfo = new TaskInfo { Title = $"Thanks {nameStep1} - Get Email", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = nextStepCard } }; return new Response(new ContinueTask(nextStepTaskInfo)); case "webpage_dialog_step_2": var nameStep2 = GetFormValue("name") ?? "Unknown"; var emailStep2 = GetFormValue("email") ?? "No email"; await client.Send($"Hi {nameStep2}, thanks for submitting the form! We got that your email is {emailStep2}"); return new Response(new MessageTask("Multi-step form completed successfully")); ``` ### Complete Multi-Step Form Handler Here's the complete example showing how to handle a multi-step form: ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[TASK_SUBMIT] Task submit request received"); var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Response(new MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; log.Info($"[TASK_SUBMIT] Submission type: {submissionType}"); string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "webpage_dialog_step_1": var nameStep1 = GetFormValue("name") ?? "Unknown"; var nextStepCardJson = $$""" { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "email", "label": "Email", "placeholder": "Enter your email", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": "{{nameStep1}}"} } ] } """; var nextStepCard = JsonSerializer.Deserialize(nextStepCardJson) ?? throw new InvalidOperationException("Failed to deserialize next step card"); var nextStepTaskInfo = new TaskInfo { Title = $"Thanks {nameStep1} - Get Email", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = nextStepCard } }; return new Response(new ContinueTask(nextStepTaskInfo)); case "webpage_dialog_step_2": var nameStep2 = GetFormValue("name") ?? "Unknown"; var emailStep2 = GetFormValue("email") ?? "No email"; await client.Send($"Hi {nameStep2}, thanks for submitting the form! We got that your email is {emailStep2}"); return new Response(new MessageTask("Multi-step form completed successfully")); default: return new Response(new MessageTask("Unknown submission type")); } } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-multi-step-forms/python.incl.md ================================================ Start off by sending an initial card in the `dialog_open` event. ```python dialog_card = AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ {"type": "TextBlock", "text": "This is a multi-step form", "size": "Large", "weight": "Bolder"}, { "type": "Input.Text", "id": "name", "label": "Name", "placeholder": "Enter your name", "isRequired": True, }, ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_1"}, } ], } ) ``` Then in the submission handler, you can choose to `continue` the dialog with a different card. ```python @app.on_dialog_submit async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): """Handle dialog submit events for all dialog types.""" data: Optional[Any] = ctx.activity.value.data dialog_type = data.get("submissiondialogtype") if data else None if dialog_type == "webpage_dialog": name = data.get("name") if data else None email = data.get("email") if data else None await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") return InvokeResponse( body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted successfully")) ) elif dialog_type == "webpage_dialog_step_1": name = data.get("name") if data else None next_step_card = AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ {"type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder"}, { "type": "Input.Text", "id": "email", "label": "Email", "placeholder": "Enter your email", "isRequired": True, }, ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": name}, } ], } ) return InvokeResponse( body=TaskModuleResponse( task=TaskModuleContinueResponse( value=CardTaskModuleTaskInfo( title=f"Thanks {name} - Get Email", card=card_attachment(AdaptiveCardAttachment(content=next_step_card)), ) ) ) ) elif dialog_type == "webpage_dialog_step_2": name = data.get("name") if data else None email = data.get("email") if data else None await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") return InvokeResponse( body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Multi-step form completed successfully")) ) return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown submission type")) ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/dialogs/handling-multi-step-forms/typescript.incl.md ================================================ Start off by sending an initial card in the `dialog.open` event. ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { AdaptiveCard, TextInput, SubmitAction } from '@microsoft/teams.cards'; // ... const dialogCard = new AdaptiveCard( { type: 'TextBlock', text: 'This is a multi-step form', size: 'Large', weight: 'Bolder', }, new TextInput() .withLabel('Name') .withIsRequired() .withId('name') .withPlaceholder('Enter your name') ) // Inside the dialog, the card actions for submitting the card must be // of type Action.Submit .withActions( new SubmitAction() .withTitle('Submit') .withData({ submissiondialogtype: 'webpage_dialog_step_1' }) ); // Return an object with the task value that renders a card return { task: { type: 'continue', value: { title: 'Multi-step Form Dialog', card: cardAttachment('adaptive', dialogCard), }, }, }; ``` Then in the submission handler, you can choose to `continue` the dialog with a different card. ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, TextInput, SubmitAction } from '@microsoft/teams.cards'; // ... app.on('dialog.submit', async ({ activity, send, next }) => { const dialogType = activity.value.data.submissiondialogtype; if (dialogType === 'webpage_dialog_step_1') { // This is data from the form that was submitted const name = activity.value.data.name; const nextStepCard = new AdaptiveCard( { type: 'TextBlock', text: 'Email', size: 'Large', weight: 'Bolder', }, new TextInput() .withLabel('Email') .withIsRequired() .withId('email') .withPlaceholder('Enter your email') ).withActions( new SubmitAction().withTitle('Submit').withData({ // This same handler will get called, so we need to identify the step // in the returned data submissiondialogtype: 'webpage_dialog_step_2', // Carry forward data from previous step name, }) ); return { task: { // This indicates that the dialog flow should continue type: 'continue', value: { // Here we customize the title based on the previous response title: `Thanks ${name} - Get Email`, card: cardAttachment('adaptive', nextStepCard), }, }, }; } else if (dialogType === 'webpage_dialog_step_2') { const name = activity.value.data.name; const email = activity.value.data.email; await send(`Hi ${name}, thanks for submitting the form! We got that your email is ${email}`); // You can also return a blank response return { status: 200, }; } }); ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/feedback/csharp.incl.md ================================================ ```csharp // This store would ideally be persisted in a database public static class FeedbackStore { public static readonly Dictionary StoredFeedbackByMessageId = new(); public class FeedbackData { public string IncomingMessage { get; set; } = string.Empty; public string OutgoingMessage { get; set; } = string.Empty; public int Likes { get; set; } public int Dislikes { get; set; } public List Feedbacks { get; set; } = new(); } } ``` ```csharp var sentMessageId = await context.Send( result.Content != null ? new MessageActivity(result.Content) .AddAiGenerated() /** Add feedback buttons via this method */ .AddFeedback() : "I did not generate a response." ); FeedbackStore.StoredFeedbackByMessageId[sentMessageId.Id] = new FeedbackStore.FeedbackData { IncomingMessage = context.Activity.Text, OutgoingMessage = result.Content ?? string.Empty, Likes = 0, Dislikes = 0, Feedbacks = new List() }; ``` ```csharp [Microsoft.Teams.Apps.Activities.Invokes.Message.Feedback] public Task OnFeedbackReceived([Context] Microsoft.Teams.Api.Activities.Invokes.Messages.SubmitActionActivity activity) { var reaction = activity.Value?.ActionValue?.GetType().GetProperty("reaction")?.GetValue(activity.Value?.ActionValue)?.ToString(); var feedbackJson = activity.Value?.ActionValue?.GetType().GetProperty("feedback")?.GetValue(activity.Value?.ActionValue)?.ToString(); if (activity.ReplyToId == null) { _log.LogWarning("No replyToId found for messageId {ActivityId}", activity.Id); return Task.CompletedTask; } var existingFeedback = FeedbackStore.StoredFeedbackByMessageId.GetValueOrDefault(activity.ReplyToId); /** * feedbackJson looks like: * {"feedbackText":"Nice!"} */ if (existingFeedback == null) { _log.LogWarning("No feedback found for messageId {ActivityId}", activity.Id); } else { var updatedFeedback = new FeedbackStore.FeedbackData { IncomingMessage = existingFeedback.IncomingMessage, OutgoingMessage = existingFeedback.OutgoingMessage, Likes = existingFeedback.Likes + (reaction == "like" ? 1 : 0), Dislikes = existingFeedback.Dislikes + (reaction == "dislike" ? 1 : 0), Feedbacks = existingFeedback.Feedbacks.Concat(new[] { feedbackJson ?? string.Empty }).ToList() }; FeedbackStore.StoredFeedbackByMessageId[activity.Id] = updatedFeedback; } return Task.CompletedTask; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/feedback/python.incl.md ================================================ Once you receive a feedback event, you can choose to store it in some persistent storage. You'll need to implement storage for tracking: - Like/dislike counts per message - Text feedback comments - Message ID associations For production applications, consider using databases, file systems, or cloud storage. The examples below use in-memory storage for simplicity. ```python from microsoft_teams.ai import Agent from microsoft_teams.api import MessageActivityInput from microsoft_teams.apps import ActivityContext, MessageActivity @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle 'feedback demo' command to demonstrate feedback collection""" agent = Agent(current_model) chat_result = await agent.send( input="Tell me a short joke", instructions="You are a comedian. Keep responses brief and funny." ) if chat_result.response.content: message = MessageActivityInput(text=chat_result.response.content) .add_ai_generated() # Create message with feedback enabled .add_feedback() await ctx.send(message) ``` ```python import json from typing import Dict, Any from microsoft_teams.api import MessageSubmitActionInvokeActivity from microsoft_teams.apps import ActivityContext # ... # Handle feedback submission events @app.on_message_submit_feedback async def handle_message_feedback(ctx: ActivityContext[MessageSubmitActionInvokeActivity]): """Handle feedback submission events""" activity = ctx.activity # Extract feedback data from activity value if not hasattr(activity, "value") or not activity.value: logger.warning(f"No value found in activity {activity.id}") return # Access feedback data directly from invoke value invoke_value = activity.value assert invoke_value.action_name == "feedback" feedback_str = invoke_value.action_value.feedback reaction = invoke_value.action_value.reaction feedback_json: Dict[str, Any] = json.loads(feedback_str) # { 'feedbackText': 'the ai response was great!' } if not activity.reply_to_id: logger.warning(f"No replyToId found for messageId {activity.id}") return # Store the feedback (implement your own storage logic) upsert_feedback_storage(activity.reply_to_id, reaction, feedback_json.get('feedbackText', '')) # Optionally Send confirmation response feedback_text: str = feedback_json.get("feedbackText", "") reaction_text: str = f" and {reaction}" if reaction else "" text_part: str = f" with comment: '{feedback_text}'" if feedback_text else "" await ctx.reply(f"✅ Thank you for your feedback{reaction_text}{text_part}!") ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/feedback/typescript.incl.md ================================================ ```typescript import { ChatPrompt, IChatModel } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity, MessageActivity } from '@microsoft/teams.api'; // ... // This store would ideally be persisted in a database export const storedFeedbackByMessageId = new Map< string, { incomingMessage: string; outgoingMessage: string; likes: number; dislikes: number; feedbacks: string[]; } >(); ``` ```typescript import { ChatPrompt, IChatModel } from '@microsoft/teams.ai'; import { ActivityLike, IMessageActivity, MessageActivity, SentActivity, } from '@microsoft/teams.api'; // ... const { id: sentMessageId } = await send( result.content != null ? new MessageActivity(result.content) .addAiGenerated() /** Add feedback buttons via this method */ .addFeedback() : 'I did not generate a response.' ); storedFeedbackByMessageId.set(sentMessageId, { incomingMessage: activity.text, outgoingMessage: result.content ?? '', likes: 0, dislikes: 0, feedbacks: [], }); ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('message.submit.feedback', async ({ activity, log }) => { const { reaction, feedback: feedbackJson } = activity.value.actionValue; if (activity.replyToId == null) { log.warn(`No replyToId found for messageId ${activity.id}`); return; } const existingFeedback = storedFeedbackByMessageId.get(activity.replyToId); /** * feedbackJson looks like: * {"feedbackText":"Nice!"} */ if (!existingFeedback) { log.warn(`No feedback found for messageId ${activity.id}`); } else { storedFeedbackByMessageId.set(activity.id, { ...existingFeedback, likes: existingFeedback.likes + (reaction === 'like' ? 1 : 0), dislikes: existingFeedback.dislikes + (reaction === 'dislike' ? 1 : 0), feedbacks: [...existingFeedback.feedbacks, feedbackJson], }); } }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/meeting-events/csharp.incl.md ================================================ ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register meeting start handler teamsApp.OnMeetingStart(async (context, cancellationToken) => { var activity = context.Activity.Value; var startTime = activity.StartTime.ToLocalTime(); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"'{activity.Title}' has started at {startTime}.") { Wrap = true, Weight = TextWeight.Bolder } }, Actions = new List { new OpenUrlAction(activity.JoinUrl) { Title = "Join the meeting", } } }; await context.Send(card, cancellationToken); }); ``` ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register meeting end handler teamsApp.OnMeetingEnd(async (context, cancellationToken) => { var activity = context.Activity.Value; var endTime = activity.EndTime.ToLocalTime(); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"'{activity.Title}' has ended at {endTime}.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register participant join handler teamsApp.OnMeetingJoin(async (context, cancellationToken) => { var activity = context.Activity.Value; var member = activity.Members[0].User.Name; var role = activity.Members[0].Meeting.Role; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"{member} has joined the meeting as {role}.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register participant leave handler teamsApp.OnMeetingLeave(async (context, cancellationToken) => { var activity = context.Activity.Value; var member = activity.Members[0].User.Name; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"{member} has left the meeting.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/meeting-events/python.incl.md ================================================ ```python from microsoft_teams.api.activities.event import MeetingStartEventActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import AdaptiveCard, OpenUrlAction, TextBlock app = App() @app.on_meeting_start async def handle_meeting_start(ctx: ActivityContext[MeetingStartEventActivity]): meeting_data = ctx.activity.value start_time = meeting_data.start_time.strftime("%c") card = AdaptiveCard( body=[ TextBlock( text=f"'{meeting_data.title}' has started at {start_time}.", wrap=True, weight="Bolder", ) ], actions=[OpenUrlAction(url=meeting_data.join_url, title="Join the meeting")], ) await ctx.send(card) ``` ```python from microsoft_teams.api.activities.event import MeetingEndEventActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import AdaptiveCard, TextBlock app = App() @app.on_meeting_end async def handle_meeting_end(ctx: ActivityContext[MeetingEndEventActivity]): meeting_data = ctx.activity.value end_time = meeting_data.end_time.strftime("%c") card = AdaptiveCard( body=[ TextBlock( text=f"'{meeting_data.title}' has ended at {end_time}.", wrap=True, weight="Bolder", ) ] ) await ctx.send(card) ``` ```python from microsoft_teams.api.activities.event import MeetingParticipantJoinEventActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import AdaptiveCard, TextBlock app = App() @app.on_meeting_participant_join async def handle_meeting_participant_join(ctx: ActivityContext[MeetingParticipantJoinEventActivity]): meeting_data = ctx.activity.value member = meeting_data.members[0].user.name role = meeting_data.members[0].meeting.role if hasattr(meeting_data.members[0].meeting, "role") else "a participant" card = AdaptiveCard( body=[ TextBlock( text=f"{member} has joined the meeting as {role}.", wrap=True, weight="Bolder", ) ] ) await ctx.send(card) ``` ```python from microsoft_teams.api.activities.event import MeetingParticipantLeaveEventActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import AdaptiveCard, TextBlock app = App() @app.on_meeting_participant_leave async def handle_meeting_participant_leave(ctx: ActivityContext[MeetingParticipantLeaveEventActivity]): meeting_data = ctx.activity.value member = meeting_data.members[0].user.name card = AdaptiveCard( body=[ TextBlock( text=f"{member} has left the meeting.", wrap=True, weight="Bolder", ) ] ) await ctx.send(card) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/meeting-events/typescript.incl.md ================================================ ```typescript import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, TextBlock, OpenUrlAction, ActionSet } from '@microsoft/teams.cards'; const app = new App(); app.on('meetingStart', async ({ activity, send }) => { const meetingData = activity.value; const startTime = new Date(meetingData.StartTime).toLocaleString(); const card = new AdaptiveCard( new TextBlock(`'${meetingData.Title}' has started at ${startTime}.`, { wrap: true, weight: 'Bolder' }), new ActionSet( new OpenUrlAction(meetingData.JoinUrl).withTitle('Join the meeting') ) ); await send(card); }); ``` ```typescript import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; const app = new App(); app.on('meetingEnd', async ({ activity, send }) => { const meetingData = activity.value; const endTime = new Date(meetingData.EndTime).toLocaleString(); const card = new AdaptiveCard( new TextBlock(`'${meetingData.Title}' has ended at ${endTime}.`, { wrap: true, weight: 'Bolder' }) ); await send(card); }); ``` ```typescript import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; const app = new App(); app.on('meetingParticipantJoin', async ({ activity, send }) => { const meetingData = activity.value; const member = meetingData.members[0].user.name; const role = meetingData.members[0].meeting.role; const card = new AdaptiveCard( new TextBlock(`${member} has joined the meeting as ${role}.`, { wrap: true, weight: 'Bolder' }) ); await send(card); }); ``` ```typescript import { App } from '@microsoft/teams.apps'; import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; const app = new App(); app.on('meetingParticipantLeave', async ({ activity, send }) => { const meetingData = activity.value; const member = meetingData.members[0].user.name; const card = new AdaptiveCard( new TextBlock(`${member} has left the meeting.`, { wrap: true, weight: 'Bolder' }) ); await send(card); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/action-commands/csharp.incl.md ================================================ Handle submission when the `createCard` or `getMessageDetails` actions commands are invoked. ```csharp using System.Text.Json; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.SubmitAction] public Response OnMessageExtensionSubmit( [Context] SubmitActionActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_SUBMIT] Action submit received"); var commandId = activity.Value?.CommandId; var data = activity.Value?.Data as JsonElement?; log.Info($"[MESSAGE_EXT_SUBMIT] Command: {commandId}"); log.Info($"[MESSAGE_EXT_SUBMIT] Data: {JsonSerializer.Serialize(data)}"); switch (commandId) { case "createCard": return HandleCreateCard(data, log); case "getMessageDetails": return HandleGetMessageDetails(activity, log); default: log.Error($"[MESSAGE_EXT_SUBMIT] Unknown command: {commandId}"); return CreateErrorActionResponse("Unknown command"); } } ``` `HandleCreateCard()` method ```csharp using System.Text.Json; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static Response HandleCreateCard(JsonElement? data, ILogger log) { var title = GetJsonValue(data, "title") ?? "Default Title"; var description = GetJsonValue(data, "description") ?? "Default Description"; log.Info($"[CREATE_CARD] Title: {title}, Description: {description}"); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Custom Card Created") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Good }, new TextBlock(title) { Weight = TextWeight.Bolder, Size = TextSize.Medium }, new TextBlock(description) { Wrap = true, IsSubtle = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.AdaptiveCard, Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` `HandleGetMessageDetails()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; //... private static Response HandleGetMessageDetails(SubmitActionActivity activity, ILogger log) { var messageText = activity.Value?.MessagePayload?.Body?.Content ?? "No message content"; var messageId = activity.Value?.MessagePayload?.Id ?? "Unknown"; log.Info($"[GET_MESSAGE_DETAILS] Message ID: {messageId}"); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Message Details") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Accent }, new TextBlock($"Message ID: {messageId}") { Wrap = true }, new TextBlock($"Content: {messageText}") { Wrap = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` Handle opening adaptive card dialog when the `fetchConversationMembers` command is invoked. ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.FetchTask] public async Task OnMessageExtensionFetchTask( [Context] FetchTaskActivity activity, [Context] ILogger log) { log.Info("[MESSAGE_EXT_FETCH_TASK] Fetch task received"); var commandId = activity.Value?.CommandId; log.Info($"[MESSAGE_EXT_FETCH_TASK] Command: {commandId}"); return CreateFetchTaskResponse(commandId, log); } ``` `CreateFetchTaskResponse()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static ActionResponse CreateFetchTaskResponse(string? commandId, ILogger log) { log.Info($"[CREATE_FETCH_TASK] Creating task for command: {commandId}"); // Create an adaptive card for the task module var card = new AdaptiveCard { Body = new List { new TextBlock("Conversation Members is not implemented in C# yet :(") { Weight = TextWeight.Bolder, Color = TextColor.Accent }, } }; return new ActionResponse { Task = new ContinueTask(new TaskInfo { Title = "Fetch Task Dialog", Height = new Union(Size.Small), Width = new Union(Size.Small), Card = new Microsoft.Teams.Api.Attachment(card) }) }; } // Helper method to extract JSON values private static string? GetJsonValue(JsonElement? data, string key) { if (data?.ValueKind == JsonValueKind.Object && data.Value.TryGetProperty(key, out var value)) { return value.GetString(); } return null; } // Helper method to create error responses private static Response CreateErrorActionResponse(string message) { return new Response { ComposeExtension = new Result { Type = ResultType.Message, Text = message } }; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/action-commands/python.incl.md ================================================ Handle submission when the `createCard` or `getMessageDetails` actions commands are invoked. ```python from microsoft_teams.api import AdaptiveCardAttachment, MessageExtensionSubmitActionInvokeActivity, card_attachment from microsoft_teams.api.models import AttachmentLayout, MessagingExtensionActionInvokeResponse, MessagingExtensionAttachment, MessagingExtensionResult, MessagingExtensionResultType from microsoft_teams.apps import ActivityContext # ... @app.on_message_ext_submit async def handle_message_ext_submit(ctx: ActivityContext[MessageExtensionSubmitActionInvokeActivity]): command_id = ctx.activity.value.command_id if command_id == "createCard": card = create_card(ctx.activity.value.data or {}) elif command_id == "getMessageDetails" and ctx.activity.value.message_payload: card = create_message_details_card(ctx.activity.value.message_payload) else: raise Exception(f"Unknown commandId: {command_id}") main_attachment = card_attachment(AdaptiveCardAttachment(content=card)) attachment = MessagingExtensionAttachment( content_type=main_attachment.content_type, content=main_attachment.content ) result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[attachment] ) return MessagingExtensionActionInvokeResponse(compose_extension=result) ``` `create_card()` method ```py from typing import Dict from microsoft_teams.cards import AdaptiveCard # ... def create_card(data: Dict[str, str]) -> AdaptiveCard: """Create an adaptive card from form data.""" return AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ {"type": "Image", "url": IMAGE_URL}, { "type": "TextBlock", "text": data.get("title", ""), "size": "Large", "weight": "Bolder", "color": "Accent", "style": "heading", }, { "type": "TextBlock", "text": data.get("subTitle", ""), "size": "Small", "weight": "Lighter", "color": "Good", }, {"type": "TextBlock", "text": data.get("text", ""), "wrap": True, "spacing": "Medium"}, ], } ) ``` `create_message_details_card()` method ```python from typing import Dict, List, Union from microsoft_teams.api.models.message import Message from microsoft_teams.cards import AdaptiveCard # ... def create_message_details_card(message_payload: Message) -> AdaptiveCard: """Create a card showing message details.""" body: List[Dict[str, Union[str, bool]]] = [ { "type": "TextBlock", "text": "Message Details", "size": "Large", "weight": "Bolder", "color": "Accent", "style": "heading", } ] if message_payload.body and message_payload.body.content: content_blocks: List[Dict[str, Union[str, bool]]] = [ {"type": "TextBlock", "text": "Content", "size": "Medium", "weight": "Bolder", "spacing": "Medium"}, {"type": "TextBlock", "text": message_payload.body.content}, ] body.extend(content_blocks) if message_payload.attachments: attachment_blocks: List[Dict[str, Union[str, bool]]] = [ {"type": "TextBlock", "text": "Attachments", "size": "Medium", "weight": "Bolder", "spacing": "Medium"}, { "type": "TextBlock", "text": f"Number of attachments: {len(message_payload.attachments)}", "wrap": True, "spacing": "Small", }, ] body.extend(attachment_blocks) if message_payload.created_date_time: date_blocks: List[Dict[str, Union[str, bool]]] = [ {"type": "TextBlock", "text": "Created Date", "size": "Medium", "weight": "Bolder", "spacing": "Medium"}, {"type": "TextBlock", "text": message_payload.created_date_time, "wrap": True, "spacing": "Small"}, ] body.extend(date_blocks) if message_payload.link_to_message: link_blocks: List[Dict[str, Union[str, bool]]] = [ {"type": "TextBlock", "text": "Message Link", "size": "Medium", "weight": "Bolder", "spacing": "Medium"} ] body.extend(link_blocks) actions = [{"type": "Action.OpenUrl", "title": "Go to message", "url": message_payload.link_to_message}] else: actions = [] return AdaptiveCard.model_validate({"type": "AdaptiveCard", "version": "1.4", "body": body, "actions": actions}) ``` Handle opening adaptive card dialog when the `fetchConversationMembers` command is invoked. ```python from microsoft_teams.api import AdaptiveCardAttachment, MessageExtensionFetchTaskInvokeActivity, card_attachment from microsoft_teams.api.models import CardTaskModuleTaskInfo, MessagingExtensionActionInvokeResponse, TaskModuleContinueResponse from microsoft_teams.apps import ActivityContext # ... @app.on_message_ext_open async def handle_message_ext_open(ctx: ActivityContext[MessageExtensionFetchTaskInvokeActivity]): conversation_id = ctx.activity.conversation.id members = await ctx.api.conversations.members(conversation_id).get_all() card = create_conversation_members_card(members) card_info = CardTaskModuleTaskInfo( title="Conversation members", height="small", width="small", card=card_attachment(AdaptiveCardAttachment(content=card)), ) task = TaskModuleContinueResponse(value=card_info) return MessagingExtensionActionInvokeResponse(task=task) ``` `create_conversation_members_card()` method ```python from typing import List from microsoft_teams.api import Account from microsoft_teams.cards import AdaptiveCard # ... def create_conversation_members_card(members: List[Account]) -> AdaptiveCard: """Create a card showing conversation members.""" members_list = ", ".join(member.name for member in members if member.name) return AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Conversation members", "size": "Medium", "weight": "Bolder", "color": "Accent", "style": "heading", }, {"type": "TextBlock", "text": members_list, "wrap": True, "spacing": "Small"}, ], } ) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/action-commands/typescript.incl.md ================================================ Handle submission when the `createCard` or `getMessageDetails` action commands are invoked. ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { IAdaptiveCard } from '@microsoft/teams.cards'; // ... app.on('message.ext.submit', async ({ activity }) => { const { commandId } = activity.value; let card: IAdaptiveCard; if (commandId === 'createCard') { // The activity.value.commandContext == "compose" here because it was from // the compose box card = createCard(activity.value.data); } else if (commandId === 'getMessageDetails' && activity.value.messagePayload) { // The activity.value.commandContext == "message" here because it was from // the message context card = createMessageDetailsCard(activity.value.messagePayload); } else { throw new Error(`Unknown commandId: ${commandId}`); } return { composeExtension: { type: 'result', attachmentLayout: 'list', attachments: [cardAttachment('adaptive', card)], }, }; }); ``` `createCard()` function ```typescript import { AdaptiveCard, TextBlock, Image } from '@microsoft/teams.cards'; // ... interface IFormData { title: string; subtitle: string; text: string; } export function createCard(data: IFormData) { return new AdaptiveCard( new Image(IMAGE_URL), new TextBlock(data.title, { size: 'Large', weight: 'Bolder', color: 'Accent', style: 'heading', }), new TextBlock(data.subtitle, { size: 'Small', weight: 'Lighter', color: 'Good', }), new TextBlock(data.text, { wrap: true, spacing: 'Medium', }) ); } ``` `createMessageDetailsCard()` function ```typescript import { Message } from '@microsoft/teams.api'; import { AdaptiveCard, CardElement, TextBlock, ActionSet, OpenUrlAction, } from '@microsoft/teams.cards'; // ... export function createMessageDetailsCard(messagePayload: Message) { const cardElements: CardElement[] = [ new TextBlock('Message Details', { size: 'Large', weight: 'Bolder', color: 'Accent', style: 'heading', }), ]; if (messagePayload?.body?.content) { cardElements.push( new TextBlock('Content', { size: 'Medium', weight: 'Bolder', spacing: 'Medium', }), new TextBlock(messagePayload.body.content) ); } if (messagePayload?.attachments?.length) { cardElements.push( new TextBlock('Attachments', { size: 'Medium', weight: 'Bolder', spacing: 'Medium', }), new TextBlock(`Number of attachments: ${messagePayload.attachments.length}`, { wrap: true, spacing: 'Small', }) ); } if (messagePayload?.createdDateTime) { cardElements.push( new TextBlock('Created Date', { size: 'Medium', weight: 'Bolder', spacing: 'Medium', }), new TextBlock(messagePayload.createdDateTime, { wrap: true, spacing: 'Small', }) ); } if (messagePayload?.linkToMessage) { cardElements.push( new TextBlock('Message Link', { size: 'Medium', weight: 'Bolder', spacing: 'Medium', }), new ActionSet( new OpenUrlAction(messagePayload.linkToMessage, { title: 'Go to message', }) ) ); } return new AdaptiveCard(...cardElements); } ``` Handle opening adaptive card dialog when the `fetchConversationMembers` command is invoked. ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... app.on('message.ext.open', async ({ activity, api }) => { const conversationId = activity.conversation.id; const members = await api.conversations.members(conversationId).get(); const card = createConversationMembersCard(members); return { task: { type: 'continue', value: { title: 'Conversation members', height: 'small', width: 'small', card: cardAttachment('adaptive', card), }, }, }; }); ``` `createConversationMembersCard()` function ```typescript import { Account } from '@microsoft/teams.api'; import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // ... export function createConversationMembersCard(members: Account[]) { const membersList = members.map((member) => member.name).join(', '); return new AdaptiveCard( new TextBlock('Conversation members', { size: 'Medium', weight: 'Bolder', color: 'Accent', style: 'heading', }), new TextBlock(membersList, { wrap: true, spacing: 'Small', }) ); } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/link-unfurling/csharp.incl.md ================================================ ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.QueryLink] public Response OnMessageExtensionQueryLink( [Context] QueryLinkActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_QUERY_LINK] Link unfurling received"); var url = activity.Value?.Url; log.Info($"[MESSAGE_EXT_QUERY_LINK] URL: {url}"); if (string.IsNullOrEmpty(url)) { return CreateErrorResponse("No URL provided"); } return CreateLinkUnfurlResponse(url, log); } ``` `CreateLinkUnfurlResponse()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; //... private static Response CreateLinkUnfurlResponse(string url, ILogger log) { var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Link Preview") { Weight = TextWeight.Bolder, Size = TextSize.Medium }, new TextBlock($"URL: {url}") { IsSubtle = true, Wrap = true }, new TextBlock("This is a preview of the linked content generated by the message extension.") { Wrap = true, Size = TextSize.Small } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } // Helper method to create error responses private static Response CreateErrorResponse(string message) { return new Response { ComposeExtension = new Result { Type = ResultType.Message, Text = message } }; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/link-unfurling/python.incl.md ================================================ ```python from microsoft_teams.api import ( AdaptiveCardAttachment, MessageExtensionQueryLinkInvokeActivity, ThumbnailCardAttachment, card_attachment, InvokeResponse, AttachmentLayout, MessagingExtensionAttachment, MessagingExtensionInvokeResponse, MessagingExtensionResult, MessagingExtensionResultType, ) from microsoft_teams.apps import ActivityContext # ... @app.on_message_ext_query_link async def handle_message_ext_query_link(ctx: ActivityContext[MessageExtensionQueryLinkInvokeActivity]): url = ctx.activity.value.url if not url: return InvokeResponse[MessagingExtensionInvokeResponse](status=400) card_data = create_link_unfurl_card(url) main_attachment = card_attachment(AdaptiveCardAttachment(content=card_data["card"])) preview_attachment = card_attachment(ThumbnailCardAttachment(content=card_data["thumbnail"])) attachment = MessagingExtensionAttachment( content_type=main_attachment.content_type, content=main_attachment.content, preview=preview_attachment, ) result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[attachment], ) return MessagingExtensionInvokeResponse(compose_extension=result) ``` `create_link_unfurl_card()` function ```python from typing import Any, Dict from microsoft_teams.cards import AdaptiveCard # ... def create_link_unfurl_card(url: str) -> Dict[str, Any]: """Create a card for link unfurling.""" thumbnail = { "title": "Unfurled Link", "text": url, "images": [{"url": IMAGE_URL}], } card = AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Unfurled Link", "size": "Large", "weight": "Bolder", "color": "Accent", "style": "heading", }, { "type": "TextBlock", "text": url, "size": "Small", "weight": "Lighter", "color": "Good", }, ], } ) return {"card": card, "thumbnail": thumbnail} ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/link-unfurling/typescript.incl.md ================================================ ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { IAdaptiveCard } from '@microsoft/teams.cards'; // ... app.on('message.ext.query-link', async ({ activity }) => { const { url } = activity.value; if (!url) { return { status: 400 }; } const { card, thumbnail } = createLinkUnfurlCard(url); const attachment = { ...cardAttachment('adaptive', card), // expanded card in the compose box... preview: cardAttachment('thumbnail', thumbnail), //preview card in the compose box... }; return { composeExtension: { type: 'result', attachmentLayout: 'list', attachments: [attachment], }, }; }); ``` `createLinkUnfurlCard()` function ```typescript import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; import { ThumbnailCard } from '@microsoft/teams.api'; // ... export function createLinkUnfurlCard(url: string) { const thumbnail = { title: 'Unfurled Link', text: url, images: [ { url: IMAGE_URL, }, ], } as ThumbnailCard; const card = new AdaptiveCard( new TextBlock('Unfurled Link', { size: 'Large', weight: 'Bolder', color: 'Accent', style: 'heading', }), new TextBlock(url, { size: 'Small', weight: 'Lighter', color: 'Good', }) ); return { card, thumbnail, }; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/search-commands/csharp.incl.md ================================================ ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.Query] public Response OnMessageExtensionQuery( [Context] QueryActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_QUERY] Search query received"); var commandId = activity.Value?.CommandId; var query = activity.Value?.Parameters?.FirstOrDefault(p => p.Name == "searchQuery")?.Value?.ToString() ?? ""; log.Info($"[MESSAGE_EXT_QUERY] Command: {commandId}, Query: {query}"); if (commandId == "searchQuery") { return CreateSearchResults(query, log); } return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List() } }; } ``` `CreateSearchResults()` method ```csharp using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static Response CreateSearchResults(string query, ILogger log) { var attachments = new List(); // Create simple search results for (int i = 1; i <= 5; i++) { var card = new AdaptiveCard { Body = new List { new TextBlock($"Search Result {i}") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextBlock($"Query: '{query}' - Result description for item {i}") { Wrap = true, IsSubtle = true } } }; var previewCard = new ThumbnailCard() { Title = $"Result {i}", Text = $"This is a preview of result {i} for query '{query}'." }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.AdaptiveCard, Content = card, Preview = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.ThumbnailCard, Content = previewCard } }; attachments.Add(attachment); } return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = attachments } }; } ``` To implement custom actions when a user clicks on a search result item, you can handle the select item event: ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; //... [MessageExtension.SelectItem] public Response OnMessageExtensionSelectItem( [Context] SelectItemActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_SELECT_ITEM] Item selection received"); var selectedItem = activity.Value; log.Info($"[MESSAGE_EXT_SELECT_ITEM] Selected: {JsonSerializer.Serialize(selectedItem)}"); return CreateItemSelectionResponse(selectedItem, log); } // Helper method to create item selection response private static Response CreateItemSelectionResponse(object? selectedItem, ILogger log) { var itemJson = JsonSerializer.Serialize(selectedItem); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Item Selected") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Good }, new TextBlock("You selected the following item:") { Wrap = true }, new TextBlock(itemJson) { Wrap = true, FontType = FontType.Monospace, Separator = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/search-commands/python.incl.md ================================================ ```python from microsoft_teams.api import AdaptiveCardAttachment, MessageExtensionQueryInvokeActivity, ThumbnailCardAttachment, card_attachment, InvokeResponse, AttachmentLayout, MessagingExtensionAttachment, MessagingExtensionInvokeResponse, MessagingExtensionResult, MessagingExtensionResultType # ... @app.on_message_ext_query async def handle_message_ext_query(ctx: ActivityContext[MessageExtensionQueryInvokeActivity]): command_id = ctx.activity.value.command_id search_query = "" if ctx.activity.value.parameters and len(ctx.activity.value.parameters) > 0: search_query = ctx.activity.value.parameters[0].value or "" if command_id == "searchQuery": cards = await create_dummy_cards(search_query) attachments: list[MessagingExtensionAttachment] = [] for card_data in cards: main_attachment = card_attachment(AdaptiveCardAttachment(content=card_data["card"])) preview_attachment = card_attachment(ThumbnailCardAttachment(content=card_data["thumbnail"])) attachment = MessagingExtensionAttachment( content_type=main_attachment.content_type, content=main_attachment.content, preview=preview_attachment ) attachments.append(attachment) result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=attachments ) return MessagingExtensionInvokeResponse(compose_extension=result) return InvokeResponse[MessagingExtensionInvokeResponse](status=400) ``` `create_dummy_cards()` method ```python from typing import Any, Dict, List from microsoft_teams.cards import AdaptiveCard # ... async def create_dummy_cards(search_query: str) -> List[Dict[str, Any]]: """Create dummy cards for search results.""" dummy_items = [ { "title": "Item 1", "description": f"This is the first item and this is your search query: {search_query}", }, {"title": "Item 2", "description": "This is the second item"}, {"title": "Item 3", "description": "This is the third item"}, {"title": "Item 4", "description": "This is the fourth item"}, {"title": "Item 5", "description": "This is the fifth item"}, ] cards: List[Dict[str, Any]] = [] for item in dummy_items: card_data: Dict[str, Any] = { "card": AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": item["title"], "size": "Large", "weight": "Bolder", "color": "Accent", "style": "heading", }, {"type": "TextBlock", "text": item["description"], "wrap": True, "spacing": "Medium"}, ], } ), "thumbnail": { "title": item["title"], "text": item["description"], }, } cards.append(card_data) return cards ``` ```python from microsoft_teams.api import MessageExtensionSelectItemInvokeActivity, AttachmentLayout, MessagingExtensionInvokeResponse, MessagingExtensionResult, MessagingExtensionResultType from microsoft_teams.apps import ActivityContext # ... @app.on_message_ext_select_item async def handle_message_ext_select_item(ctx: ActivityContext[MessageExtensionSelectItemInvokeActivity]): option = getattr(ctx.activity.value, "option", None) await ctx.send(f"Selected item: {option}") result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[] ) return MessagingExtensionInvokeResponse(compose_extension=result) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/search-commands/typescript.incl.md ================================================ ```typescript import { cardAttachment } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; // ... app.on('message.ext.query', async ({ activity }) => { const { commandId } = activity.value; const searchQuery = activity.value.parameters![0].value; if (commandId == 'searchQuery') { const cards = await createDummyCards(searchQuery); const attachments = cards.map(({ card, thumbnail }) => { return { ...cardAttachment('adaptive', card), // expanded card in the compose box... preview: cardAttachment('thumbnail', thumbnail), // preview card in the compose box... }; }); return { composeExtension: { type: 'result', attachmentLayout: 'list', attachments: attachments, }, }; } return { status: 400 }; }); ``` `createDummyCards()` function ```typescript import { ThumbnailCard } from '@microsoft/teams.api'; import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // ... export async function createDummyCards(searchQuery: string) { const dummyItems = [ { title: 'Item 1', description: `This is the first item and this is your search query: ${searchQuery}`, }, { title: 'Item 2', description: 'This is the second item' }, { title: 'Item 3', description: 'This is the third item' }, { title: 'Item 4', description: 'This is the fourth item' }, { title: 'Item 5', description: 'This is the fifth item' }, ]; const cards = dummyItems.map((item) => { return { card: new AdaptiveCard( new TextBlock(item.title, { size: 'Large', weight: 'Bolder', color: 'Accent', style: 'heading', }), new TextBlock(item.description, { wrap: true, spacing: 'Medium', }) ), thumbnail: { title: item.title, text: item.description, // When a user clicks on a list item in Teams: // - If the thumbnail has a `tap` property: Teams will trigger the `message.ext.select-item` activity // - If no `tap` property: Teams will insert the full adaptive card into the compose box // tap: { // type: "invoke", // title: item.title, // value: { // "option": index, // }, // }, } satisfies ThumbnailCard, }; }); return cards; } ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('message.ext.select-item', async ({ activity, send }) => { const { option } = activity.value; await send(`Selected item: ${option}`); return { status: 200, }; }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/settings/csharp.incl.md ================================================ ```html Message Extension Settings

Message Extension Settings

``` ```csharp // In your startup configuration (Program.cs or Startup.cs) app.UseStaticFiles(); app.MapGet("/tabs/settings", async context => { var html = await File.ReadAllTextAsync("wwwroot/settings.html"); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html); }); ``` :::note This will serve the HTML page to the `${BOT_ENDPOINT}/tabs/settings` endpoint as a tab. See [Tabs Guide](../tabs) to learn more. ::: ```csharp using Microsoft.Teams.Api.Cards; using Microsoft.Teams.Cards; [MessageExtension.QuerySettingsUrl] public Microsoft.Teams.Api.MessageExtensions.Response OnMessageExtensionQuerySettingsUrl( [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.QuerySettingsUrlActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) { log.Info("[MESSAGE_EXT_QUERY_SETTINGS_URL] Settings URL query received"); // Get user settings (this could come from a database or user store) var selectedOption = ""; // Default or retrieve from user preferences var botEndpoint = Environment.GetEnvironmentVariable("BOT_ENDPOINT") ?? "https://your-bot-endpoint.com"; var settingsUrl = $"{botEndpoint}/tabs/settings?selectedOption={Uri.EscapeDataString(selectedOption)}"; var settingsAction = new CardAction { Type = CardActionType.OpenUrl, Title = "Settings", Value = settingsUrl }; var suggestedActions = new Microsoft.Teams.Api.MessageExtensions.SuggestedActions { Actions = new List { settingsAction } }; var result = new Microsoft.Teams.Api.MessageExtensions.Result { Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Config, SuggestedActions = suggestedActions }; return new Microsoft.Teams.Api.MessageExtensions.Response { ComposeExtension = result }; } ``` ```csharp [MessageExtension.Setting] public Microsoft.Teams.Api.MessageExtensions.Response OnMessageExtensionSetting( [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.SettingActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) { log.Info("[MESSAGE_EXT_SETTING] Settings submission received"); var state = activity.Value?.State; log.Info($"[MESSAGE_EXT_SETTING] State: {state}"); if (state == "CancelledByUser") { log.Info("[MESSAGE_EXT_SETTING] User cancelled settings"); return CreateEmptyResult(); } var selectedOption = state; log.Info($"[MESSAGE_EXT_SETTING] Selected option: {selectedOption}"); // Here you would typically save the user's settings to a database or user store // SaveUserSettings(activity.From.Id, selectedOption); // Return empty result to close the settings dialog return CreateEmptyResult(); } // Helper method to create empty result private static Microsoft.Teams.Api.MessageExtensions.Response CreateEmptyResult() { return new Microsoft.Teams.Api.MessageExtensions.Response { ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result { Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result, AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List, Attachments = new List() } }; } ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/settings/python.incl.md ================================================ ```html Message Extension Settings

Message Extension Settings

``` ```python app.page("settings", str(Path(__file__).parent), "/tabs/settings") ``` ```python @app.on_message_ext_query_settings_url async def handle_message_ext_query_settings_url(ctx: ActivityContext[MessageExtensionQuerySettingUrlInvokeActivity]): user_settings = {"selectedOption": ""} escaped_selected_option = user_settings["selectedOption"] bot_endpoint = os.environ.get("BOT_ENDPOINT", "") settings_action = CardAction( type=CardActionType.OPEN_URL, title="Settings", value=f"{bot_endpoint}/tabs/settings?selectedOption={escaped_selected_option}", ) suggested_actions = MessagingExtensionSuggestedAction(actions=[settings_action]) result = MessagingExtensionResult(type=MessagingExtensionResultType.CONFIG, suggested_actions=suggested_actions) return MessagingExtensionInvokeResponse(compose_extension=result) ``` ```python @app.on_message_ext_setting async def handle_message_ext_setting(ctx: ActivityContext[MessageExtensionSettingInvokeActivity]): state = getattr(ctx.activity.value, "state", None) if state == "CancelledByUser": result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[] ) return MessagingExtensionInvokeResponse(compose_extension=result) selected_option = state await ctx.send(f"Selected option: {selected_option}") result = MessagingExtensionResult( type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[] ) return MessagingExtensionInvokeResponse(compose_extension=result) ``` :::note This will serve the HTML page to the `${BOT_ENDPOINT}/tabs/settings` endpoint as a tab. ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/message-extensions/settings/typescript.incl.md ================================================ ```html
What programming language do you prefer? Typescript
C#


``` ```typescript import path from 'path'; import { App } from '@microsoft/teams.apps'; // ... app.tab('settings', path.resolve(__dirname)); ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('message.ext.query-settings-url', async ({ activity }) => { // Get user settings from storage if available const userSettings = (await app.storage.get(activity.from.id)) || { selectedOption: '' }; const escapedSelectedOption = encodeURIComponent(userSettings.selectedOption); return { composeExtension: { type: 'config', suggestedActions: { actions: [ { type: 'openUrl', title: 'Settings', // ensure the bot endpoint is set in the environment variables // process.env.BOT_ENDPOINT is not populated by default in the Teams Toolkit setup. value: `${process.env.BOT_ENDPOINT}/tabs/settings?selectedOption=${escapedSelectedOption}`, }, ], }, }, }; }); ``` ```typescript import { App } from '@microsoft/teams.apps'; // ... app.on('message.ext.setting', async ({ activity, send }) => { const { state } = activity.value; if (state == 'CancelledByUser') { return { status: 400, }; } const selectedOption = state; // Save the selected option to storage await app.storage.set(activity.from.id, { selectedOption }); await send(`Selected option: ${selectedOption}`); return { status: 200, }; }); ``` :::note This will serve the HTML page to the `${BOT_ENDPOINT}/tabs/settings` endpoint as a tab. See [Tabs Guide](../tabs) to learn more. ::: ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/logging/csharp.incl.md ================================================ `ConsoleLogger` `Microsoft.Teams.Common` ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Common.Logging; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); var appBuilder = App.Builder() .AddLogger(new ConsoleLogger()) builder.AddTeams(appBuilder) var app = builder.Build(); var teams = app.UseTeams(); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/logging/python.incl.md ================================================ `ConsoleLogger` `microsoft-teams-common` ```python import asyncio from microsoft_teams.api import MessageActivity from microsoft_teams.api.activities.typing import TypingActivityInput from microsoft_teams.apps import ActivityContext, App from microsoft_teams.common import ConsoleLogger, ConsoleLoggerOptions logger = ConsoleLogger().create_logger("echo", ConsoleLoggerOptions(level="debug")) app = App(logger=logger) @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): logger.debug(ctx.activity) await ctx.reply(TypingActivityInput()) await ctx.send(f"You said '{ctx.activity.text}'") if __name__ == "__main__": asyncio.run(app.start()) ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/logging/typescript.incl.md ================================================ `ConsoleLogger` `@microsoft/teams.common` ```typescript import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common'; // initialize app with custom console logger // set to debug log level const app = new App({ logger: new ConsoleLogger('echo', { level: 'debug' }), }); app.on('message', async ({ send, activity, log }) => { log.debug(activity); await send({ type: 'typing' }); await send(`you said "${activity.text}"`); }); (async () => { await app.start(); })(); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/middleware/csharp.incl.md ================================================ `app.Use` ```csharp app.Use(async context => { var start = DateTime.UtcNow; try { await context.Next(); } catch { context.Log.Error("error occurred during activity processing"); } context.Log.Debug($"request took {(DateTime.UtcNow - start).TotalMilliseconds}ms"); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/middleware/python.incl.md ================================================ `app.use` ```python @app.use async def log_activity(ctx: ActivityContext[MessageActivity]): started_at = datetime.now() await ctx.next() ctx.logger.debug(f"{datetime.now() - started_at}") ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/observability/middleware/typescript.incl.md ================================================ `app.use` ```typescript app.use(async ({ log, next }) => { const startedAt = new Date(); await next(); log.debug(new Date().getTime() - startedAt.getTime()); }); ``` ================================================ FILE: teams.md/src/components/include/in-depth-guides/python.incl.md ================================================ This documentation covers advanced features and capabilities of the Teams SDK in Python. ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/http-server/csharp.incl.md ================================================ N/A N/A N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/http-server/python.incl.md ================================================ [FastAPI](https://fastapi.tiangolo.com/) ```python class HttpServerAdapter(Protocol): def register_route(self, method: HttpMethod, path: str, handler: HttpRouteHandler) -> None: ... def serve_static(self, path: str, directory: str) -> None: ... async def start(self, port: int) -> None: ... async def stop(self) -> None: ... class HttpRouteHandler(Protocol): async def __call__(self, request: HttpRequest) -> HttpResponse: ... ``` ```python import asyncio import uvicorn from fastapi import FastAPI from microsoft_teams.apps import App, FastAPIAdapter # 1. Create your FastAPI app with your own routes my_fastapi = FastAPI(title="My App + Teams Bot") @my_fastapi.get("/health") async def health(): return {"status": "healthy"} # 2. Wrap it in the FastAPIAdapter adapter = FastAPIAdapter(app=my_fastapi) # 3. Create the Teams app with the adapter app = App(http_server_adapter=adapter) @app.on_message async def handle_message(ctx): await ctx.send(f"Echo: {ctx.activity.text}") async def main(): # 4. Initialize — registers /api/messages on your FastAPI app (does NOT start a server) await app.initialize() # 5. Start the server yourself config = uvicorn.Config(app=my_fastapi, host="0.0.0.0", port=3978) server = uvicorn.Server(config) await server.serve() asyncio.run(main()) ``` > See the full example: [FastAPI non-managed example](https://github.com/microsoft/teams.py/tree/main/examples/http-adapters/src/fastapi_non_managed.py) Here is a Starlette adapter — only `register_route` is needed: ```python from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse, Response from starlette.routing import Route from microsoft_teams.apps.http.adapter import HttpMethod, HttpRequest, HttpResponse, HttpRouteHandler class StarletteAdapter: def __init__(self, app: Starlette): self._app = app def register_route(self, method: HttpMethod, path: str, handler: HttpRouteHandler) -> None: # Teams only sends POST requests to your bot endpoint async def starlette_handler(request: Request) -> Response: body = await request.json() headers = dict(request.headers) result: HttpResponse = await handler(HttpRequest(body=body, headers=headers)) if result.get("body") is not None: return JSONResponse(content=result["body"], status_code=result["status"]) return Response(status_code=result["status"]) route = Route(path, starlette_handler, methods=[method]) self._app.routes.insert(0, route) ``` Usage: ```python starlette_app = Starlette() adapter = StarletteAdapter(starlette_app) app = App(http_server_adapter=adapter) await app.initialize() # Start Starlette with uvicorn yourself ``` > See the full implementation: [Starlette adapter example](https://github.com/microsoft/teams.py/tree/main/examples/http-adapters/src/starlette_adapter.py) ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/http-server/typescript.incl.md ================================================ [Express](https://expressjs.com/) ```typescript interface IHttpServerAdapter { registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void; serveStatic?(path: string, directory: string): void; start?(port: number): Promise; stop?(): Promise; } type HttpRouteHandler = (request: { body: unknown; headers: Record }) => Promise<{ status: number; body?: unknown }>; ``` ```typescript import http from 'http'; import express from 'express'; import { App, ExpressAdapter } from '@microsoft/teams.apps'; // 1. Create your Express app with your own routes const expressApp = express(); const httpServer = http.createServer(expressApp); expressApp.get('/health', (_req, res) => { res.json({ status: 'healthy' }); }); // 2. Wrap it in the ExpressAdapter const adapter = new ExpressAdapter(httpServer); // 3. Create the Teams app with the adapter const app = new App({ httpServerAdapter: adapter }); app.on('message', async ({ send, activity }) => { await send(`Echo: ${activity.text}`); }); // 4. Initialize — registers /api/messages on your Express app (does NOT start a server) await app.initialize(); // 5. Start the server yourself httpServer.listen(3978, () => console.log('Server ready on http://localhost:3978')); ``` > See the full [Express adapter example](https://github.com/microsoft/teams.ts/tree/main/examples/http-adapters/express) Here is a Restify adapter — only `registerRoute` is needed: ```typescript import restify from 'restify'; import { HttpMethod, IHttpServerAdapter, HttpRouteHandler } from '@microsoft/teams.apps'; class RestifyAdapter implements IHttpServerAdapter { constructor(private server: restify.Server) { this.server.use(restify.plugins.bodyParser()); } registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void { // Teams only sends POST requests to your bot endpoint assert(method === 'POST', `Unsupported method: ${method}`); this.server.post(path, async (req: restify.Request, res: restify.Response) => { const response = await handler({ body: req.body, headers: req.headers as Record, }); res.send(response.status, response.body); }); } } ``` Usage: ```typescript const server = restify.createServer(); const adapter = new RestifyAdapter(server); const app = new App({ httpServerAdapter: adapter }); await app.initialize(); server.listen(3978); ``` > See the full implementation: [Restify adapter example](https://github.com/microsoft/teams.ts/tree/main/examples/http-adapters/restify) ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/static-pages/csharp.incl.md ================================================ `app.AddTab()` ```csharp app.AddTab("myApp", "Web/bin"); ``` `http://localhost:{PORT}/tabs/myApp` or `https://{BOT_DOMAIN}/tabs/myApp` - For more details about Tab apps, see the [Tabs](../tabs/) in-depth guide. - For an example of hosting a Dialog, see the [Creating Dialogs](../dialogs/creating-dialogs) in-depth guide. ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/static-pages/python.incl.md ================================================ `app.tab()` ```python app.tab("my_app", os.path.abspath("dist/client")) ``` `http://localhost:{PORT}/tabs/my_app` or `https://{BOT_DOMAIN}/tabs/my_app` - For an example of hosting a Dialog, see the [Creating Dialogs](../dialogs/creating-dialogs) in-depth guide. ================================================ FILE: teams.md/src/components/include/in-depth-guides/server/static-pages/typescript.incl.md ================================================ `app.tab()` ```typescript app.tab('myApp', path.resolve('dist/client')); ``` `http://localhost:{PORT}/tabs/myApp` or `https://{BOT_DOMAIN}/tabs/myApp` - For more details about Tab apps, see the [Tabs](../tabs) in-depth guide. - For an example of hosting a Dialog, see the [Creating Dialogs](../dialogs/creating-dialogs) in-depth guide. ================================================ FILE: teams.md/src/components/include/in-depth-guides/tabs/csharp.incl.md ================================================ This SDK does not offer features for implementing Tab apps in C#. It does however let you host tab apps and implement functions that can be called by Tab apps. ### Additional resources - [Static Pages](../server/static-pages) - [TypeScript Tabs in-depth guide](../../../typescript/in-depth-guides/tabs) ================================================ FILE: teams.md/src/components/include/in-depth-guides/tabs/functions/csharp.incl.md ================================================ Agents may want to expose REST APIs that client applications can call. This SDK makes it easy to implement those APIs through the `app.AddFunction()` method. The function takes a name and a callback that implements the function. ```csharp app.AddFunction('do-something', (context) => { // do something useful }); ``` This registers a REST API hosted at `http://localhost:{PORT}/api/functions/do-something` or `https://{BOT_DOMAIN}/api/functions/do-something` that clients can POST to. When they do, this SDK validates that the caller provides a valid Microsoft Entra bearer token before invoking the registered callback. If the token is missing or invalid, the request is denied with a HTTP 401. The function can be typed to accept input arguments. The clients would include those in the POST request payload, and they are made available in the callback through the `Data` context argument. ```csharp public class ProcessMessageData { [JsonPropertyName("message")] public required string Message { get; set; } } // ... app.AddFunction ("process-message", (context) => { context.Log.Debug($"process-message with: {context.Data.Message}"); }); ``` :::warning This SDK does not validate that the function arguments are of the expected types or otherwise trustworthy. You must take care to validate the input arguments before using them. ::: If desired, the function can return data to the caller. ```csharp app.AddFunction('get-random-number', () => { return 4; // chosen by fair dice roll; // guaranteed to be random }); ``` The function callback receives a context object with a number of useful values. Some originate within the agent itself, while others are furnished by the caller via the HTTP Request. | Property | Source | Description | | -------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | | `Api` | Agent | The API client. | | `AppId` | Agent | Unique identifier assigned to the app after deployment, ensuring correct app instance recognition across hosts. | | `AppSessionId` | Caller | Unique ID for the calling app's session, used to correlate telemetry data. | | `AuthToken` | Caller | The validated MSAL Entra token. | | `ChannelId` | Caller | Microsoft Teams ID for the channel associated with the content. | | `ChatId` | Caller | Microsoft Teams ID for the chat associated with the content. | | `Data` | Caller | The function payload. | | `Log` | Agent | The app logger instance. | | `MeetingId` | Caller | Meeting ID used by tab when running in meeting context. | | `MessageId` | Caller | ID of the parent message from which the task module was launched (only available in bot card-launched modules). | | `PageId` | Caller | Developer-defined unique ID for the page this content points to. | | `Send` | Agent | Sends an activity to the current conversation. | | `SubPageId` | Caller | Developer-defined unique ID for the sub-page this content points to. Used to restore specific state within a page. | | `TeamId` | Caller | Microsoft Teams ID for the team associated with the content. | | `TenantId` | Caller | Microsoft Entra tenant ID of the current user, extracted from the validated auth token. | | `UserId` | Caller | Microsoft Entra object ID of the current user, extracted from the validated auth token. | | `UserName` | Caller | Microsoft Entra name of the current user, extracted from the validated auth token. | The `AuthToken` is validated before the function callback is invoked, and the `TenantId`, `UserId`, and `UserName` values are extracted from the validated token. In the typical case, the remaining caller-supplied values would reflect what the Teams Tab app retrieves from the teams-js `getContext()` API, but the agent does not validate these. :::warning Take care to validate the caller-supplied values before using them. Don't assume that the calling user actually has access to items indicated in the context. ::: To simplify a common scenarios, the context provides a `Send` method. This method sends an activity to the current conversation ID, determined from the context values provided by the client (chatId and channelId). If neither chatId or channelId is provided by the caller, the ID of the 1:1 conversation between the agent and the user is assumed. :::warning The `Send` method does not validate that the chat ID or conversation ID provided by the caller is valid or correct. You must take care to validate that the user and agent both have appropriate access to the conversation. ::: - For details on how to Tab apps can call these functions, see the TypeScript [Executing Functions](../../../../typescript/in-depth-guides/tabs/functions/function-calling) in-depth guide. - For more information about the teams-js getContext() API, see the [Teams JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) documentation. ================================================ FILE: teams.md/src/components/include/in-depth-guides/tabs/functions/typescript.incl.md ================================================ Agents may want to expose REST APIs that client applications can call. This SDK makes it easy to implement those APIs through the `app.function()` method. The function takes a name and a callback that implements the function. ```typescript app.function('do-something', () => { // do something useful }); ``` This registers a REST API hosted at `http://localhost:{PORT}/api/functions/do-something` or `https://{BOT_DOMAIN}/api/functions/do-something` that clients can POST to. When they do, this SDK validates that the caller provides a valid Microsoft Entra bearer token before invoking the registered callback. If the token is missing or invalid, the request is denied with a HTTP 401. The function can be typed to accept input arguments. The clients would include those in the POST request payload, and they are made available in the callback through the `data` context argument. ```typescript app.function<{}, { message: string }>('process-message', ({ data, log }) => { log.info(`process-message called with: ${data.message}`); }); ``` :::warning This SDK does not validate that the function arguments are of the expected types or otherwise trustworthy. You must take care to validate the input arguments before using them. ::: If desired, the function can return data to the caller. The return value can be a string, an object, or an array. ```typescript app.function('get-random-number', () => { return '4'; // chosen by fair dice roll; // guaranteed to be random }); ``` If your function returns a number, that will be interpreted as an HTTP status code: ```typescript app.function('privileged-action', ({ userId }) => { if (!hasPermission(userId)) { return 401; // HTTP response will have status 401: unauthorized } // ... do something }); ``` The function callback receives a context object with a number of useful values. Some originate within the agent itself, while others are furnished by the caller via the HTTP Request. | Property | Source | Description | | -------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `api` | Agent | The API client. | | `appGraph` | Agent | The app graph client. | | `appId` | Agent | Unique identifier assigned to the app after deployment, ensuring correct app instance recognition across hosts. | | `appSessionId` | Caller | Unique ID for the calling app's session, used to correlate telemetry data. | | `authToken` | Caller | The validated MSAL Entra token. | | `channelId` | Caller | Microsoft Teams ID for the channel associated with the content. | | `chatId` | Caller | Microsoft Teams ID for the chat associated with the content. | | `data` | Caller | The function payload. | | `getCurrentConversationId` | Agent | Attempts to find the conversation ID where the app is used and verifies agent-user presence. Returns `undefined` if not found or invalid. | | `log` | Agent | The app logger instance. | | `meetingId` | Caller | Meeting ID used by tab when running in meeting context. | | `messageId` | Caller | ID of the parent message from which the task module was launched (only available in bot card-launched modules). | | `pageId` | Caller | Developer-defined unique ID for the page this content points to. | | `send` | Agent | Sends an activity to the current conversation. Returns `null` if the conversation ID is invalid or undetermined. | | `subPageId` | Caller | Developer-defined unique ID for the sub-page this content points to. Used to restore specific state within a page. | | `teamId` | Caller | Microsoft Teams ID for the team associated with the content. | | `tenantId` | Caller | Microsoft Entra tenant ID of the current user, extracted from the validated auth token. | | `userId` | Caller | Microsoft Entra object ID of the current user, extracted from the validated auth token. | The `authToken` is validated before the function callback is invoked, and the `tenantId` and `userId` values are extracted from the validated token. In the typical case, the remaining caller-supplied values would reflect what the Teams Tab app retrieves from the teams-js `getContext()` API, but the agent does not validate them. :::warning Take care to validate the caller-supplied values before using them. Don't assume that the calling user actually has access to items indicated in the context. ::: To simplify two common scenarios, the context provides the `getCurrentConversationId` and `send` methods. - The `getCurrentConversationId` method attempts to find the current conversation ID based on the context provided by the client (chatId and channelId) and validates that both the agent and the calling user are actually present in the conversation. If neither chatId or channelId is provided by the caller, the ID of the 1:1 conversation between the agent and the user is returned. - The `send` method relies on `getCurrentConversationId` to find the conversation where the app is hosted and posts an activity. - For details on how to Tab apps can invoke these functions, see the [Executing Functions](./function-calling) in-depth guide. ================================================ FILE: teams.md/src/components/include/in-depth-guides/tabs/typescript.incl.md ================================================ Tab apps will often need to interact with remote services. They may need to fetch data from [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview) or invoke remote agent functions, using the [Nested App Authentication](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/authentication/nested-authentication) (NAA) and the [Microsoft Authentication Library](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) (MSAL) to ensure user consent and to allow the remote service authenticate the user. The `@microsoft/teams.client` package in this SDK builds on TeamsJS and MSAL to streamline these common scenarios. It aims to simplify: - **Remote Service Authentication** through MSAL-based authentication and token acquisition. - **Graph API Integration** by offering a pre-configured and type-safe Microsoft Graph client. - **Agent Function Calling** by handling authentication and including app context when calling server-side functions implemented Teams SDK agents. - **Scope Consent Management** by providing simple APIs to test for and request user consent. ### Additional resources - [Static Pages](../server/static-pages) ================================================ FILE: teams.md/src/components/include/in-depth-guides/typescript.incl.md ================================================ This documentation covers advanced features and capabilities of the Teams SDK in TypeScript. ================================================ FILE: teams.md/src/components/include/in-depth-guides/user-authentication/csharp.incl.md ================================================ Use your terminal to run the following command: ```sh npx @microsoft/teams.cli@latest new csharp oauth-app --template graph ``` ```cs var builder = WebApplication.CreateBuilder(args); var appBuilder = App.Builder() .AddOAuth("graph"); builder.AddTeams(appBuilder); var app = builder.Build(); var teams = app.UseTeams(); ``` ```cs teams.OnMessage("/signin", async (context, cancellationToken) => { if (context.IsSignedIn) { await context.Send("you are already signed in!", cancellationToken); return; } else { await context.SignIn(cancellationToken); } }); ``` ```cs teams.OnSignIn(async (_, teamsEvent, cancellationToken) => { var context = teamsEvent.Context; await context.Send($"Signed in using OAuth connection {context.ConnectionName}. Please type **/whoami** to see your profile or **/signout** to sign out.", cancellationToken); }); ``` ```cs teams.OnMessage("/whoami", async (context, cancellationToken) => { if (!context.IsSignedIn) { await context.Send("you are not signed in!. Please type **/signin** to sign in", cancellationToken); return; } var me = await context.GetUserGraphClient().Me.GetAsync(); await context.Send($"user \"{me!.DisplayName}\" signed in.", cancellationToken); }); teams.OnMessage(async (context, cancellationToken) => { if (context.IsSignedIn) { await context.Send($"You said : {context.Activity.Text}. Please type **/whoami** to see your profile or **/signout** to sign out.", cancellationToken); } else { await context.Send($"You said : {context.Activity.Text}. Please type **/signin** to sign in.", cancellationToken); } }); ``` ```cs teams.OnMessage("/signout", async (context, cancellationToken) => { if (!context.IsSignedIn) { await context.Send("you are not signed in!", cancellationToken); return; } await context.SignOut(cancellationToken); await context.Send("you have been signed out!", cancellationToken); }); ``` ```cs teams.OnSignInFailure(async (context, cancellationToken) => { var failure = context.Activity.Value; Console.WriteLine($"Sign-in failed: {failure?.Code} - {failure?.Message}"); await context.Send("Sign-in failed.", cancellationToken); }); ``` N/A ================================================ FILE: teams.md/src/components/include/in-depth-guides/user-authentication/python.incl.md ================================================ Use your terminal to run the following command: ```sh npx @microsoft/teams.cli@latest new python oauth-app --template graph ``` This command: 1. Creates a new directory called `oauth-app`. 2. Bootstraps the graph agent template files into it under `oauth-app/src`. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `oauth-app/appPackage` directory. ```python from teams import App from teams.api import MessageActivity, SignInEvent from teams.apps import ActivityContext from teams.logger import ConsoleLogger, ConsoleLoggerOptions app = App( # The name of the auth connection to use. # It should be the same as the Oauth connection name defined in the Azure Bot configuration. default_connection_name="graph", logger=ConsoleLogger().create_logger("auth", options=ConsoleLoggerOptions(level="debug"))) ``` ```python @app.on_message async def handle_signin_message(ctx: ActivityContext[MessageActivity]): """Handle message activities for signing in.""" ctx.logger.info("User requested sign-in.") if ctx.is_signed_in: await ctx.send("You are already signed in.") else: await ctx.sign_in() ``` ```python @app.event("sign_in") async def handle_sign_in(event: SignInEvent): """Handle sign-in events.""" await event.activity_ctx.send("You are now signed in!") ``` ```python @app.on_message async def handle_whoami_message(ctx: ActivityContext[MessageActivity]): """Handle messages to show user information from Microsoft Graph.""" if not ctx.is_signed_in: await ctx.send("You are not signed in! Please sign in to continue.") return # Access user's Microsoft Graph data me = await ctx.user_graph.me.get() await ctx.send(f"Hello {me.display_name}! Your email is {me.mail or me.user_principal_name}") @app.on_message async def handle_all_messages(ctx: ActivityContext[MessageActivity]): """Handle all other messages.""" if ctx.is_signed_in: await ctx.send(f'You said: "{ctx.activity.text}". Please type **/whoami** to see your profile or **/signout** to sign out.') else: await ctx.send(f'You said: "{ctx.activity.text}". Please type **/signin** to sign in.') ``` ```python @app.on_message async def handle_signout_message(ctx: ActivityContext[MessageActivity]): """Handle sign out requests.""" if not ctx.is_signed_in: await ctx.send("You are not signed in!") return await ctx.sign_out() await ctx.send("You have been signed out!") ``` ```python @app.on_signin_failure() async def handle_signin_failure(ctx): failure = ctx.activity.value print(f"Sign-in failed: {failure.code} - {failure.message}") await ctx.send("Sign-in failed.") ``` :::note In Python, registering a custom handler does **not** replace the built-in default handler. Both will run as part of the middleware chain. ::: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ## Regional Configs You may be building a regional bot that is deployed in a specific Azure region (such as West Europe, East US, etc.) rather than global. This is important for organizations that have data residency requirements or want to reduce latency by keeping data and authentication flows within a specific area. These examples use West Europe, but follow the equivalent for other regions. To configure a new regional bot in Azure, you must setup your resoures in the desired region. Your resource group must also be in the same region. 1. Deploy a new App Registration in `westeurope`. 2. Deploy and link a new Enterprise Application (Service Principal) on Microsoft Entra in `westeurope`. 3. Deploy and link a new Azure Bot in `westeurope`. 4. In your App Registration, in the `Authentication (Preview)` tab, add a `Redirect URI` for the Platform Type `Web` to your regional endpoint (e.g., `https://europe.token.botframework.com/.auth/web/redirect`) ![Authentication Tab](/screenshots/regional-auth.png) 5. In your `.env` file (or wherever you set your environment variables), add your `OAUTH_URL`. For example: `OAUTH_URL=https://europe.token.botframework.com` To configure a new regional bot with ATK, you will need to make a few updates. Note that this assumes you have not yet deployed the bot previously. 1. In `azurebot.bicep`, replace all `global` occurrences to `westeurope` 2. In `manifest.json`, in `validDomains`, `*.botframework.com` should be replaced by `europe.token.botframework.com` 3. In `aad.manifest.json`, replace `https://token.botframework.com/.auth/web/redirect` with `https://europe.token.botframework.com/.auth/web/redirect` 4. In your `.env` file, add your `OAUTH_URL`. For example: `OAUTH_URL=https://europe.token.botframework.com`. ================================================ FILE: teams.md/src/components/include/in-depth-guides/user-authentication/typescript.incl.md ================================================ Use your terminal to run the following command: ```sh npx @microsoft/teams.cli@latest new typescript oauth-app --template graph ``` This command: 1. Creates a new directory called `oauth-app`. 2. Bootstraps the graph agent template files into it under `oauth-app/src`. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `oauth-app/appPackage` directory. ```ts import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ oauth: { defaultConnectionName: 'graph', }, }); ``` ```ts app.message('/signin', async ({ signin, send }) => { if (await signin()) { await send('you are already signed in!'); } }); ``` ```ts app.event('signin', async ({ send, token }) => { await send( `Signed in using OAuth connection ${token.connectionName}. Please type **/whoami** to see your profile or **/signout** to sign out.` ); }); ``` ```ts import * as endpoints from '@microsoft/teams.graph-endpoints'; app.message('/whoami', async ({ send, userGraph, signin }) => { if (!await signin()) { return; } const me = await userGraph.call(endpoints.me.get); await send( `you are signed in as "${me.displayName}" and your email is "${me.mail || me.userPrincipalName}"` ); }); app.on('message', async ({ send, activity, signin }) => { if (await signin()) { await send( `You said: "${activity.text}". Please type **/whoami** to see your profile or **/signout** to sign out.` ); } else { await send(`You said: "${activity.text}". Please type **/signin** to sign in.`); } }); ``` ```ts app.message('/signout', async ({ send, signout, isSignedIn }) => { if (!isSignedIn) return; await signout(); await send('you have been signed out!'); }); ``` ```ts app.on('signin.failure', async ({ activity, send }) => { const { code, message } = activity.value; console.log(`Sign-in failed: ${code} - ${message}`); await send('Sign-in failed.'); }); ``` import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ## Regional Configs You may be building a regional bot that is deployed in a specific Azure region (such as West Europe, East US, etc.) rather than global. This is important for organizations that have data residency requirements or want to reduce latency by keeping data and authentication flows within a specific area. These examples use West Europe, but follow the equivalent for other regions. To configure a new regional bot in Azure, you must setup your resoures in the desired region. Your resource group must also be in the same region. 1. Deploy a new App Registration in `westeurope`. 2. Deploy and link a new Enterprise Application (Service Principal) on Microsoft Entra in `westeurope`. 3. Deploy and link a new Azure Bot in `westeurope`. 4. In your App Registration, in the `Authentication (Preview)` tab, add a `Redirect URI` for the Platform Type `Web` to your regional endpoint (e.g., `https://europe.token.botframework.com/.auth/web/redirect`) ![Authentication Tab](/screenshots/regional-auth.png) 5. In your `.env` file (or wherever you set your environment variables), add your `OAUTH_URL`. For example: `OAUTH_URL=https://europe.token.botframework.com` To configure a new regional bot with ATK, you will need to make a few updates. Note that this assumes you have not yet deployed the bot previously. 1. In `azurebot.bicep`, replace all `global` occurrences to `westeurope` 2. In `manifest.json`, in `validDomains`, `*.botframework.com` should be replaced by `europe.token.botframework.com` 3. In `aad.manifest.json`, replace `https://token.botframework.com/.auth/web/redirect` with `https://europe.token.botframework.com/.auth/web/redirect` 4. In your `.env` file, add your `OAUTH_URL`. For example: `OAUTH_URL=https://europe.token.botframework.com` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/integration/csharp.incl.md ================================================ ```csharp using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Plugins.AspNetCore.Extensions; public static partial class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder .AddTeams() // highlight-next-line .AddBotBuilder(); var app = builder.Build(); var teams = app.UseTeams(); app.Run(); } teams.OnMessage(async (context, cancellationToken) => { await context.Client.Typing(cancellationToken); await context.Client.Send($"hi from teams...", cancellationToken); }); } ``` ```csharp using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Connector.Authentication; // replace with your Adapter // highlight-start public class BotBuilderAdapter : CloudAdapter { public BotBuilderAdapter(BotFrameworkAuthentication auth, ILogger logger) : base(auth, logger) { OnTurnError = async (turnContext, exception) => { logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); // Send a message to the user await turnContext.SendActivityAsync("The bot encountered an error or bug."); }; } } // highlight-end ``` ```csharp using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; // replace with your ActivityHandler // highlight-start public class Bot : ActivityHandler { protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var replyText = $"hi from botbuilder..."; await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken); } } // highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/integration/python.incl.md ================================================ ```python import asyncio from adapter import adapter from activity_handler import MyActivityHandler from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.botbuilder import BotBuilderPlugin # highlight-next-line app = App(plugins=[BotBuilderPlugin(adapter=adapter, handler=MyActivityHandler())]) @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): print("Handling message in app...") await ctx.send("hi from teams...") if __name__ == "__main__": asyncio.run(app.start()) ``` ```python from botbuilder.core import TurnContext from botbuilder.integration.aiohttp import ( CloudAdapter, ConfigurationBotFrameworkAuthentication, ) from botbuilder.schema import Activity, ActivityTypes from types import SimpleNamespace config = SimpleNamespace( APP_TYPE="SingleTenant" if tenant_id else "MultiTenant", APP_ID=client_id, APP_PASSWORD=client_secret, APP_TENANTID=tenant_id, ) # replace with your Adapter # highlight-start adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) async def on_error(context: TurnContext, error: Exception): # Send a message to the user await context.send_activity("The bot encountered an error or bug.") adapter.on_turn_error = on_error # highlight-end ``` ```python from botbuilder.core import ActivityHandler, TurnContext # replace with your ActivityHandler # highlight-start class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): await turn_context.send_activity("hi from botbuilder...") # highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/integration/typescript.incl.md ================================================ ```typescript import { App } from '@microsoft/teams.apps'; import { BotBuilderPlugin } from '@microsoft/teams.botbuilder'; import adapter from './adapter'; import handler from './activity-handler'; const app = new App({ // highlight-next-line plugins: [new BotBuilderPlugin({ adapter, handler })], }); app.on('message', async ({ send }) => { await send('hi from teams...'); }); (async () => { await app.start(); })(); ``` ```typescript import { CloudAdapter } from 'botbuilder'; // replace with your BotAdapter // highlight-start const adapter = new CloudAdapter( new ConfigurationBotFrameworkAuthentication( {}, new ConfigurationServiceClientCredentialFactory({ MicrosoftAppType: tenantId ? 'SingleTenant' : 'MultiTenant', MicrosoftAppId: clientId, MicrosoftAppPassword: clientSecret, MicrosoftAppTenantId: tenantId, }) ) ); // highlight-end export default adapter; ``` ```typescript import { TeamsActivityHandler } from 'botbuilder'; // replace with your TeamsActivityHandler // highlight-start export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (ctx, next) => { await ctx.sendActivity('hi from botbuilder...'); await next(); }); } } // highlight-end const handler = new ActivityHandler(); export default handler; ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/proactive-activities/csharp.incl.md ================================================ ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Integration.AspNet.Core; - using Microsoft.Bot.Schema; // highlight-error-end // highlight-success-line + using Microsoft.Teams.Apps; // highlight-error-start - var conversationReference = new ConversationReference - { - ServiceUrl = "...", - Bot = new ChannelAccount { ... }, - ChannelId = "msteams", - Conversation = new ConversationAccount { ... }, - User = new ChannelAccount { ... } - }; - - await adapter.ContinueConversationAsync( - configuration["MicrosoftAppId"], - conversationReference, - async (turnContext, cancellationToken) => - { - await turnContext.SendActivityAsync("proactive hello", cancellationToken: cancellationToken); - }, - default); // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + await teams.Send("your-conversation-id", "proactive hello"); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Schema; // highlight-start var conversationReference = new ConversationReference { ServiceUrl = "...", Bot = new ChannelAccount { ... }, ChannelId = "msteams", Conversation = new ConversationAccount { ... }, User = new ChannelAccount { ... } }; await adapter.ContinueConversationAsync( configuration["MicrosoftAppId"], conversationReference, async (turnContext, cancellationToken) => { await turnContext.SendActivityAsync("proactive hello", cancellationToken: cancellationToken); }, default); // highlight-end ``` ```csharp showLineNumbers using Microsoft.Teams.Apps; // highlight-start var teams = app.UseTeams(); await teams.Send("your-conversation-id", "proactive hello"); // highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/proactive-activities/python.incl.md ================================================ ```python # highlight-error-start - from botbuilder.core import TurnContext - from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication - from botbuilder.schema import ChannelAccount, ConversationAccount, ConversationReference # highlight-error-end # highlight-success-line + from microsoft_teams.apps import App # highlight-error-start - adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) # highlight-error-end # highlight-success-line + app = App() # highlight-error-start - conversation_reference = ConversationReference( - service_url="...", - bot=ChannelAccount(...), - channel_id="msteams", - conversation=ConversationAccount(...), - user=ChannelAccount(...) - ) - - async def send_proactive(turn_context: TurnContext): - await turn_context.send_activity("proactive hello") - - await adapter.continue_conversation( - conversation_reference, - send_proactive, - ) # highlight-error-end # highlight-success-start + await app.send("your-conversation-id", "proactive hello") # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import TurnContext from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication from botbuilder.schema import ChannelAccount, ConversationAccount, ConversationReference adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) # highlight-start conversation_reference = ConversationReference( service_url="...", bot=ChannelAccount(...), channel_id="msteams", conversation=ConversationAccount(...), user=ChannelAccount(...) ) async def send_proactive(turn_context: TurnContext): await turn_context.send_activity("proactive hello") await adapter.continue_conversation( conversation_reference, send_proactive ) # highlight-end ``` ```python showLineNumbers from microsoft_teams.apps import App app = App() # highlight-start await app.send("your-conversation-id", "proactive hello") # highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/proactive-activities/typescript.incl.md ================================================ ```typescript // highlight-error-start - import { - CloudAdapter, - ConfigurationBotFrameworkAuthentication, - ConversationReference, - } from 'botbuilder'; // highlight-error-end // highlight-success-line + import { App } from '@microsoft/teams.apps'; // highlight-error-start - const auth = new ConfigurationBotFrameworkAuthentication(process.env); - const adapter = new CloudAdapter(auth); // highlight-error-end // highlight-success-line + const app = new App(); (async () => { // highlight-error-start - const conversationReference: ConversationReference = { - serviceUrl: '...', - bot: { ... }, - channelId: 'msteams', - conversation: { ... }, - user: { ... }, - }; - await adapter.continueConversationAsync(process.env.MicrosoftAppId ?? '', conversationReference, async context => { - await context.sendActivity('proactive hello'); - }); // highlight-error-end // highlight-success-start + await app.start(); + await app.send('your-conversation-id', 'proactive hello'); // highlight-success-end }()); ``` ```typescript showLineNumbers import { CloudAdapter, ConfigurationBotFrameworkAuthentication, ConversationReference, } from 'botbuilder'; const auth = new ConfigurationBotFrameworkAuthentication(process.env); const adapter = new CloudAdapter(auth); // highlight-start (async () => { const conversationReference: ConversationReference = { serviceUrl: '...', bot: { ... }, channelId: 'msteams', conversation: { ... }, user: { ... }, }; await adapter.continueConversationAsync(process.env.MicrosoftAppId ?? '', conversationReference, async context => { await context.sendActivity('proactive hello'); }); }()); // highlight-end ``` ```typescript showLineNumbers import { App } from '@microsoft/teams.apps'; const app = new App(); // highlight-start (async () => { await app.start(); await app.send('your-conversation-id', 'proactive hello'); }()); // highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/sending-activities/csharp.incl.md ================================================ ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; // highlight-error-end // highlight-success-start + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; + using Microsoft.Teams.Api.Activities; //highlight-success-end // highlight-error-start - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - await turnContext.SendActivityAsync( - Activity.CreateTypingActivity(), - cancellationToken: cancellationToken); - } - } // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + teams.OnMessage(async (context, cancellationToken) => + { + await context.Send(new Activity(type:"typing"), cancellationToken); + }); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; public class MyActivityHandler : ActivityHandler { protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { // highlight-next-line await turnContext.SendActivityAsync( Activity.CreateTypingActivity(), cancellationToken: cancellationToken); } } ``` ```csharp showLineNumbers using Microsoft.Teams.Apps; using Microsoft.Teams.Plugins.AspNetCore.Extensions; using Microsoft.Teams.Api.Activities; var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { // highlight-next-line await context.Send(new Activity(type:"typing"), cancellationToken); }); ``` ## Strings ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; // highlight-error-end // highlight-success-start + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; //highlight-success-end // highlight-error-start - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - await turnContext.SendActivityAsync("hello world", cancellationToken: cancellationToken); - } - } // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + teams.OnMessage(async (context, cancellationToken) => + { + await context.Send("hello world", cancellationToken); + }); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; public class MyActivityHandler : ActivityHandler { protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { // highlight-next-line await turnContext.SendActivityAsync("hello world", cancellationToken: cancellationToken); } } ``` ```csharp showLineNumbers using Microsoft.Teams.Apps; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { // highlight-next-line await context.Send("hello world", cancellationToken); }); ``` ## Adaptive Cards ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; // highlight-error-end // highlight-success-start + using Microsoft.Teams.Apps; + using Microsoft.Teams.Cards; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; // highlight-success-end // highlight-error-start - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - var card = new - { - type = "AdaptiveCard", - version = "1.0", - body = new[] - { - new { type = "TextBlock", text = "hello world" } - } - }; - var attachment = new Attachment - { - ContentType = "application/vnd.microsoft.card.adaptive", - Content = card - }; - var activity = MessageFactory.Attachment(attachment); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - } - } // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + teams.OnMessage(async (context, cancellationToken) => + { + await context.Send(new AdaptiveCard(new TextBlock("hello world")), cancellationToken); + }); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; public class MyActivityHandler : ActivityHandler { protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { // highlight-start var card = new { type = "AdaptiveCard", version = "1.0", body = new[] { new { type = "TextBlock", text = "hello world" } } }; var attachment = new Attachment { ContentType = "application/vnd.microsoft.card.adaptive", Content = card }; var activity = MessageFactory.Attachment(attachment); await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); // highlight-end } } ``` ```csharp showLineNumbers using Microsoft.Teams.Cards; using Microsoft.Teams.Apps; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { // highlight-next-line await context.Send(new AdaptiveCard(new TextBlock("hello world")), cancellationToken); }); ``` ## Attachments ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; // highlight-error-end // highlight-success-start + using Microsoft.Teams.Apps; + using Microsoft.Teams.Api; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; // highlight-success-end // highlight-error-start - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - var activity = MessageFactory.Attachment(new Attachment { /* ... */ }); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - } - } // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + teams.OnMessage(async (context, cancellationToken) => + { + var activity = new MessageActivity(); + activity.AddAttachment(new Attachment { /* ... */ }); + await context.SendAsync(activity, cancellationToken); + }); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; public class MyActivityHandler : ActivityHandler { protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { // highlight-start var activity = MessageFactory.Attachment(new Attachment { /* ... */ }); await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); // highlight-end } } ``` ```csharp showLineNumbers using Microsoft.Teams.Api; using Microsoft.Teams.Apps; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { // highlight-start var activity = new MessageActivity(); activity.AddAttachment(new Attachment { /* ... */ }); await context.SendAsync(activity, cancellationToken); // highlight-end }); ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/sending-activities/python.incl.md ================================================ ```python # highlight-error-start - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity # highlight-error-end # highlight-success-start + from microsoft_teams.api import MessageActivity, TypingActivityInput + from microsoft_teams.apps import ActivityContext, App # highlight-success-end # highlight-error-start - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - await turn_context.send_activity(Activity(type="typing")) # highlight-error-end # highlight-success-start + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + await context.send(TypingActivityInput()) # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import ActivityHandler, TurnContext from botbuilder.schema import Activity class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): # highlight-next-line await turn_context.send_activity(Activity(type="typing")) ``` ```python showLineNumbers from microsoft_teams.api import MessageActivity, TypingActivityInput from microsoft_teams.apps import ActivityContext, App @app.on_message async def on_message(context: ActivityContext[MessageActivity]): # highlight-next-line await context.send(TypingActivityInput()) ``` ## Strings ```python # highlight-error-start - from botbuilder.core import ActivityHandler, TurnContext # highlight-error-end # highlight-success-start + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App # highlight-success-end # highlight-error-start - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - await turn_context.send_activity("hello world") # highlight-error-end # highlight-success-start + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + await context.send("hello world") # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import ActivityHandler, TurnContext class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): # highlight-next-line await turn_context.send_activity("hello world") ``` ```python showLineNumbers from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext, App @app.on_message async def on_message(context: ActivityContext[MessageActivity]): # highlight-next-line await context.send("hello world") ``` ## Adaptive Cards ```python # highlight-error-start - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity, Attachment # highlight-error-end # highlight-success-start + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App + from microsoft_teams.cards import AdaptiveCard, TextBlock # highlight-success-end # highlight-error-start - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - card = {"type": "AdaptiveCard", "version": "1.0", "body": [{"type": "TextBlock", "text": "hello world"}]} - attachment = Attachment(content_type="application/vnd.microsoft.card.adaptive", content=card) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) # highlight-error-end # highlight-success-start + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + await context.send(AdaptiveCard().with_body([TextBlock(text="Hello from Adaptive Card!")])) # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import ActivityHandler, TurnContext from botbuilder.schema import Activity, Attachment class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): # hightlight-start card = {"type": "AdaptiveCard", "version": "1.0", "body": [{"type": "TextBlock", "text": "hello world"}]} attachment = Attachment(content_type="application/vnd.microsoft.card.adaptive", content=card) activity = Activity(type="message", attachments=[attachment]) await turn_context.send_activity(activity) # highlight-end ``` ```python showLineNumbers from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import AdaptiveCard, TextBlock @app.on_message async def on_message(context: ActivityContext[MessageActivity]): # highlight-next-line await context.send(AdaptiveCard(body=[TextBlock(text="Hello from Adaptive Card!")])) ``` ## Attachments ```python # highlight-error-start - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity, Attachment # highlight-error-end # highlight-success-start + from microsoft_teams.api import Attachment, MessageActivity, MessageActivityInput + from microsoft_teams.apps import ActivityContext, App # highlight-success-end # highlight-error-start - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - attachment = Attachment(...) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) # highlight-error-end # highlight-success-start + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + attachment = Attachment(...) + activity = MessageActivityInput().add_attachments([attachment]) + await context.send(activity) # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import ActivityHandler, TurnContext from botbuilder.schema import Activity, Attachment class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): # highlight-start attachment = Attachment(...) activity = Activity(type="message", attachments=[attachment]) await turn_context.send_activity(activity) # highlight-end ``` ```python showLineNumbers from microsoft_teams.api import Attachment, MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext, App @app.on_message async def on_message(context: ActivityContext[MessageActivity]): # highlight-start attachment = Attachment(...) activity = MessageActivityInput().add_attachments([attachment]) await context.send(activity) # highlight-end ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/sending-activities/typescript.incl.md ================================================ ```typescript // highlight-error-start - import { TeamsActivityHandler } from 'botbuilder'; - export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity({ type: 'typing' }); - }); - } - } // highlight-error-end // highlight-success-start + app.on('message', async ({ send }) => { + await send({ type: 'typing' }); + }); // highlight-success-end ``` ```typescript showLineNumbers import { TeamsActivityHandler } from 'botbuilder'; export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (context) => { // highlight-next-line await context.sendActivity({ type: 'typing' }); }); } } ``` ```typescript showLineNumbers app.on('message', async ({ send }) => { // highlight-next-line await send({ type: 'typing' }); }); ``` ## Strings ```typescript // highlight-error-start - import { TeamsActivityHandler } from 'botbuilder'; - export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity('hello world'); - }); - } - } // highlight-error-end // highlight-success-start + app.on('message', async ({ send }) => { + await send('hello world'); + }); // highlight-success-end ``` ```typescript showLineNumbers import { TeamsActivityHandler } from 'botbuilder'; export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (context) => { // highlight-next-line await context.sendActivity('hello world'); }); } } ``` ```typescript showLineNumbers app.on('message', async ({ send }) => { // highlight-next-line await send('hello world'); }); ``` ## Adaptive Cards ```typescript // highlight-error-line - import { TeamsActivityHandler, CardFactory } from 'botbuilder'; // highlight-success-line + import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // highlight-error-start - export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity({ - type: 'message', - attachments: [ - CardFactory.adaptiveCard({ - $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', - type: 'AdaptiveCard', - version: '1.0', - body: [{ - type: 'TextBlock', - text: 'hello world' - }] - }) - ] - }); - }); - } - } // highlight-error-end // highlight-success-start + app.on('message', async ({ send }) => { + await send(new AdaptiveCard(new TextBlock('hello world'))); + }); // highlight-success-end ``` ```typescript showLineNumbers import { TeamsActivityHandler, CardFactory } from 'botbuilder'; export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (context) => { // highlight-start await context.sendActivity({ type: 'message', attachments: [ CardFactory.adaptiveCard({ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', type: 'AdaptiveCard', version: '1.0', body: [{ type: 'TextBlock', text: 'hello world' }] }) ] }); // highlight-end }); } } ``` ```typescript showLineNumbers import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; app.on('message', async ({ send }) => { // highlight-next-line await send(new AdaptiveCard(new TextBlock('hello world'))); }); ``` ## Attachments ```typescript // highlight-error-line - import { TeamsActivityHandler } from 'botbuilder'; // highlight-success-line + import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // highlight-error-start - export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity({ - type: 'message', - attachments: [ - ... - ] - }); - }); - } - } // highlight-error-end // highlight-success-start + app.on('message', async ({ send }) => { + await send(new MessageActivity().addAttachment(...)); + }); // highlight-success-end ``` ```typescript showLineNumbers import { TeamsActivityHandler } from 'botbuilder'; export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (context) => { // highlight-start await context.sendActivity({ type: 'message', attachments: [ ... ] }); // highlight-end }); } } ``` ```typescript showLineNumbers app.on('message', async ({ send }) => { // highlight-next-line await send(new MessageActivity().addAttachment(...)); }); ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/the-api-client/csharp.incl.md ================================================ ```csharp // highlight-error-start - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Teams; // highlight-error-end // highlight-success-line + using Microsoft.Teams.Apps; // highlight-error-start - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - var members = await TeamsInfo.GetMembersAsync(turnContext, cancellationToken); - } - } // highlight-error-end // highlight-success-start + var teams = app.UseTeams(); + teams.OnMessage(async (context, cancellationToken) => + { + var members = await context.Api.Conversations.Members.GetAsync(context.Activity.Conversation.Id); + }); // highlight-success-end ``` ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Teams; public class MyActivityHandler : TeamsActivityHandler { protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { // highlight-next-line var members = await TeamsInfo.GetMembersAsync(turnContext, cancellationToken); } } ``` ```csharp showLineNumbers using Microsoft.Teams.Apps; app.OnMessage(async (context, cancellationToken) => { // highlight-next-line var members = await context.Api.Conversations.Members.GetAsync(context.Activity.Conversation.Id); }); ``` | BotBuilder (TeamsInfo) | Teams SDK (ApiClient) | |------------------------|----------------------| | `TeamsInfo.GetMemberAsync(context, userId)` | `Api.Conversations.Members.GetByIdAsync(conversationId, userId)` | | `TeamsInfo.GetTeamDetailsAsync(context, teamId)` | `Api.Teams.GetByIdAsync(teamId)` | | `TeamsInfo.GetMeetingInfoAsync(context, meetingId)` | `Api.Meetings.GetByIdAsync(meetingId)` | | `TeamsInfo.SendMessageToTeamsChannelAsync(context, teamId, message)` | `Api.Conversations.CreateAsync(CreateRequest)` then `Api.Conversations.Activities.CreateAsync(conversationId, activity)` | ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/the-api-client/python.incl.md ================================================ ```python # highlight-error-start - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.core.teams import TeamsInfo # highlight-error-end # highlight-success-line + from microsoft_teams.apps import ActivityContext + from microsoft_teams.api import MessageActivity # highlight-error-start - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - members = await TeamsInfo.get_members(turn_context) # highlight-error-end # highlight-success-start + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + members = await context.api.conversations.members(context.activity.conversation.id).get_all() # highlight-success-end ``` ```python showLineNumbers from botbuilder.core import ActivityHandler, TurnContext from botbuilder.core.teams import TeamsInfo class MyActivityHandler(ActivityHandler): async def on_message_activity(self, turn_context: TurnContext): # highlight-next-line members = await TeamsInfo.get_members(turn_context) ``` ```python showLineNumbers from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext @app.on_message async def on_message(context: ActivityContext[MessageActivity]): # highlight-next-line members = await context.api.conversations.members(context.activity.conversation.id).get() ``` | BotBuilder (TeamsInfo) | Teams SDK (ApiClient) | |------------------------|----------------------| | `TeamsInfo.getMembers(context, user_id)` | `api.conversations.members(conversation_id).get(user_id)` | | `TeamsInfo.get_team_details(context, team_id)` | `api.teams.get_by_id(team_id)` | | `TeamsInfo.get_meeting_info(context, meeting_id)` | `api.meetings.get_by_id(meeting_id)` | | `TeamsInfo.send_message_to_teams_channel(context, team_id, message)` | `api.conversations.create(CreateConversationParams)` then `api.conversations.activities(conversation_id).create(activity)` | ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/the-api-client/typescript.incl.md ================================================ ```typescript // highlight-error-start - import { - CloudAdapter, - ConfigurationBotFrameworkAuthentication, - TeamsInfo, - } from 'botbuilder'; // highlight-error-end // highlight-success-line + import { App } from '@microsoft/teams.apps'; // highlight-error-start - const auth = new ConfigurationBotFrameworkAuthentication(process.env); - const adapter = new CloudAdapter(auth); // highlight-error-end // highlight-success-line + const app = new App(); // highlight-error-start - export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - const members = await TeamsInfo.getMembers(context); - }); - } - } // highlight-error-end // highlight-success-start + app.on('message', async ({ api, activity }) => { + const members = await api.conversations.members(activity.conversation.id).get(); + }); // highlight-success-end ``` ```typescript showLineNumbers import { CloudAdapter, ConfigurationBotFrameworkAuthentication, TeamsInfo, } from 'botbuilder'; const auth = new ConfigurationBotFrameworkAuthentication(process.env); const adapter = new CloudAdapter(auth); export class ActivityHandler extends TeamsActivityHandler { constructor() { super(); this.onMessage(async (context) => { // highlight-next-line const members = await TeamsInfo.getMembers(context); }); } } ``` ```typescript showLineNumbers import { App } from '@microsoft/teams.apps'; const app = new App(); app.on('message', async ({ api, activity }) => { // highlight-next-line const members = await api.conversations.members(activity.conversation.id).get(); }); ``` | BotBuilder (TeamsInfo) | Teams SDK (ApiClient) | |------------------------|----------------------| | `TeamsInfo.getMember(context, userId)` | `api.conversations.members(conversationId).getById(userId)` | | `TeamsInfo.getTeamDetails(context, teamId)` | `api.teams.getById(teamId)` | | `TeamsInfo.getMeetingInfo(context, meetingId)` | `api.meetings.getById(meetingId)` | | `TeamsInfo.sendMessageToTeamsChannel(context, teamId, message)` | `api.conversations.create(CreateConversationParams)` then `api.conversations.activities(conversationId).create(activity)` | ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/user-authentication/csharp.incl.md ================================================ ```csharp showLineNumbers using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Schema; public class MyActivityHandler : TeamsActivityHandler { private readonly ConversationState _conversationState; private readonly UserState _userState; private readonly Dialog _dialog; public MyActivityHandler(string connectionName, ConversationState conversationState, UserState userState) { _conversationState = conversationState; _userState = userState; _dialog = new SignInDialog("signin", connectionName); } protected override async Task OnMessageActivityAsync( ITurnContext turnContext, CancellationToken cancellationToken) { await _dialog.RunAsync(turnContext, _conversationState.CreateProperty("DialogState"), cancellationToken); } public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await base.OnTurnAsync(turnContext, cancellationToken); await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); await _userState.SaveChangesAsync(turnContext, false, cancellationToken); } } public class SignInDialog : ComponentDialog { private readonly string _connectionName; public SignInDialog(string id, string connectionName) : base(id) { _connectionName = connectionName; AddDialog(new OAuthPrompt("OAuthPrompt", new OAuthPromptSettings { ConnectionName = connectionName, Text = "Please Sign In", Title = "Sign In", Timeout = 300000 })); AddDialog(new WaterfallDialog("Main", new WaterfallStep[] { PromptStepAsync, LoginStepAsync })); InitialDialogId = "Main"; } private async Task PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { return await stepContext.BeginDialogAsync("OAuthPrompt", null, cancellationToken); } private async Task LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { await stepContext.Context.SendActivityAsync("You have been signed in.", cancellationToken: cancellationToken); return await stepContext.EndDialogAsync(null, cancellationToken); } } var storage = new MemoryStorage(); var conversationState = new ConversationState(storage); var userState = new UserState(storage); var handler = new MyActivityHandler( builder.Configuration["ConnectionName"], conversationState, userState ); ``` ```csharp showLineNumbers using Microsoft.Teams.Apps; var builder = WebApplication.CreateBuilder(args); var appBuilder = App.Builder().AddOAuth("ConnectionName"); var app = builder.Build(); var teams = app.UseTeams(); teams.OnMessage("/signout", async (context, cancellationToken) => { if (!context.IsSignedIn) return; await context.SignOut(cancellationToken); await context.Send("You have been signed out.", cancellationToken); }); teams.OnMessage(async (context, cancellationToken) => { if (!context.IsSignedIn) { await context.SignIn(cancellationToken); return; } }); teams.OnSignIn(async (_, @event, cancellationToken) => { await context.Send("You have been signed in.", cancellationToken); }); app.Run() ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/user-authentication/python.incl.md ================================================ ```python showLineNumbers from botbuilder.core import ( ActivityHandler, ConversationState, UserState, MemoryStorage, BotFrameworkAdapter ) from botbuilder.dialogs import ( ComponentDialog, OAuthPrompt, OAuthPromptSettings, WaterfallDialog, WaterfallStepContext, DialogSet, DialogTurnStatus ) class MyActivityHandler(ActivityHandler): def __init__(self, connection_name: str, conversation_state: ConversationState, user_state: UserState): super().__init__() self.conversation_state = conversation_state self.user_state = user_state self.dialog = SignInDialog("signin", connection_name) self.dialog_state = self.conversation_state.create_property("DialogState") async def on_message_activity(self, turn_context: TurnContext): await self.dialog.run(turn_context, self.dialog_state) 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) class SignInDialog(ComponentDialog): def __init__(self, dialog_id: str, connection_name: str): super().__init__(dialog_id) self.connection_name = connection_name self.add_dialog(OAuthPrompt( "OAuthPrompt", OAuthPromptSettings( connection_name=connection_name, text="Please Sign In", title="Sign In", timeout=300000 ) )) self.add_dialog(WaterfallDialog( "Main", [self.prompt_step, self.login_step] )) self.initial_dialog_id = "Main" async def prompt_step(self, step_context: WaterfallStepContext): return await step_context.begin_dialog("OAuthPrompt") async def login_step(self, step_context: WaterfallStepContext): await step_context.context.send_activity("You have been signed in.") return await step_context.end_dialog() async def run(self, turn_context: TurnContext, accessor): dialog_set = DialogSet(accessor) dialog_set.add(self) 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(self.id) storage = MemoryStorage() conversation_state = ConversationState(storage) user_state = UserState(storage) handler = MyActivityHandler( connection_name, conversation_state, user_state ) ``` ```python showLineNumbers from microsoft_teams.apps import ActivityContext, App, SignInEvent from microsoft_teams.api import MessageActivity app = App(default_connection_name=connection_name) @app.on_message_pattern("/signout") async def on_signout(context: ActivityContext[MessageActivity]): if not context.is_signed_in: return await context.sign_out() await context.send("You have been signed out.") @app.on_message async def on_message(context: ActivityContext[MessageActivity]): if not context.is_signed_in: await context.sign_in() return @app.event("sign_in") async def on_signin(event: SignInEvent): await context.send("You have been signed in.") ``` ================================================ FILE: teams.md/src/components/include/migrations/botbuilder/user-authentication/typescript.incl.md ================================================ ```typescript showLineNumbers import restify from 'restify'; import { TeamsActivityHandler, ConversationState, UserState, StatePropertyAccessor, CloudAdapter, ConfigurationBotFrameworkAuthentication, MemoryStorage, } from 'botbuilder'; import { OAuthPrompt, WaterfallDialog, ComponentDialog } from 'botbuilder-dialogs'; export class ActivityHandler extends TeamsActivityHandler { private readonly _conversationState: ConversationState; private readonly _userState: UserState; private readonly _dialog: SignInDialog; private readonly _dialogState: StatePropertyAccessor; constructor(connectionName: string, conversationState: ConversationState, userState: UserState) { super(); this._conversationState = conversationState; this._userState = userState; this._dialog = new SignInDialog('signin', connectionName); this._dialogState = this.conversationState.createProperty('DialogState'); this.onMessage(async (context, next) => { await this._dialog.run(context, this._dialogState); return next(); }); } async run(context) { await super.run(context); await this.conversationState.saveChanges(context, false); await this.userState.saveChanges(context, false); } } export class SignInDialog extends ComponentDialog { private readonly _connectionName: string; constructor(id, connectionName: string) { super(id); this._connectionName = connectionName; this.addDialog(new OAuthPrompt('OAuthPrompt', { connectionName: connectionName, text: 'Please Sign In', title: 'Sign In', timeout: 300000 })); this.addDialog(new WaterfallDialog('Main', [ this.promptStep.bind(this), this.loginStep.bind(this) ])); this.initialDialogId = 'Main'; } async run(context, accessor) { const dialogSet = new DialogSet(accessor); dialogSet.add(this); const dialogContext = await dialogSet.createContext(context); const results = await dialogContext.continueDialog(); if (results.status === DialogTurnStatus.empty) { await dialogContext.beginDialog(this.id); } } async promptStep(stepContext) { return await stepContext.beginDialog('OAuthPrompt'); } async loginStep(stepContext) { await stepContext.context.sendActivity('You have been signed in.'); return await stepContext.endDialog(); } async onBeginDialog(innerDc, options) { const result = await this.interrupt(innerDc); if (result) return result; return await super.onBeginDialog(innerDc, options); } async onContinueDialog(innerDc) { const result = await this.interrupt(innerDc); if (result) return result; return await super.onContinueDialog(innerDc); } async interrupt(innerDc) { if (innerDc.context.activity.type === ActivityTypes.Message) { const text = innerDc.context.activity.text.toLowerCase(); if (text === '/signout') { const userTokenClient = innerDc.context.turnState.get(innerDc.context.adapter.UserTokenClientKey); const { activity } = innerDc.context; await userTokenClient.signOutUser(activity.from.id, this.connectionName, activity.channelId); await innerDc.context.sendActivity('You have been signed out.'); return await innerDc.cancelAllDialogs(); } } } } const server = restify.createServer(); const auth = new ConfigurationBotFrameworkAuthentication(process.env); const adapter = new CloudAdapter(auth); const memoryStorage = new MemoryStorage(); const conversationState = new ConversationState(memoryStorage); const userState = new UserState(memoryStorage); const handler = new ActivityHandler( process.env.connectionName, conversationState, userState, ); server.use(restify.plugins.bodyParser()); server.listen(process.env.port || process.env.PORT || 3978, function() { console.log(`\n${ server.name } listening to ${ server.url }`); }); server.post('/api/messages', async (req, res) => { await adapter.process(req, res, (context) => bot.run(context)); }); ``` ```typescript showLineNumbers import { App } from '@microsoft/teams.apps'; import { ConsoleLogger } from '@microsoft/teams.common/logging'; const app = new App({ oauth: { defaultConnectionName: process.env.connectionName } }); app.message('/signout', async ({ send, signout, isSignedIn }) => { if (!isSignedIn) return; await signout(); await send('You have been signed out.'); }); app.on('message', async ({ send, signin }) => { if (!await signin()) { return; } }); app.event('signin', async ({ send }) => { await send('You have been signed in.'); }); (async () => { await app.start(); })(); ``` ================================================ FILE: teams.md/src/components/include/migrations/slack-bolt/typescript.incl.md ================================================ First, let's install the Teams SDK into your project. This will install the Teams SDK alongside any existing packages. After you've completed your migration, you can safely remove the `@microsoft/teams-ai` dependency from your `package.json` file. ```sh npm install @microsoft/teams.apps ``` First, let's configure the `App` class in Teams JS. This is equivalent to Slack Bolt's `App` class. ```ts // Setup app // highlight-error-start import { App } from '@slack/bolt'; const app = new App({ signingSecret: process.env.SLACK_SIGNING_SECRET, clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, scopes: [ "channels:manage", "channels:read", "chat:write", "groups:read", "incoming-webhook", ], installerOptions: { authVersion: "v2", directInstall: false, installPath: "/slack/install", metadata: "", redirectUriPath: "/slack/oauth_redirect", stateVerification: "true", /** * Example pages to navigate to on certain callbacks. */ callbackOptions: { success: (installation, installUrlOptions, req, res) => { res.send("The installation succeeded!"); }, failure: (error, installUrlOptions, req, res) => { res.send("Something strange happened..."); }, }, /** * Example validation of installation options using a random state and an * expiration time between requests. */ stateStore: { generateStateParam: async (installUrlOptions, now) => { const state = randomStringGenerator(); const value = { options: installUrlOptions, now: now.toJSON() }; await database.set(state, value); return state; }, verifyStateParam: async (now, state) => { const value = await database.get(state); const generated = new Date(value.now); const seconds = Math.floor( (now.getTime() - generated.getTime()) / 1000, ); if (seconds > 600) { throw new Error("The state expired after 10 minutes!"); } return value.options; }, }, }, }); // highlight-error-end // highlight-success-start import { App } from '@microsoft/teams.apps'; // Define app const app = new App({ clientId: process.env.ENTRA_APP_CLIENT_ID!, clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!, tenantId: process.env.ENTRA_TENANT_ID!, }); // highlight-success-end // App starts local server with route for /api/messages (async () => { await app.start(); })(); ``` ```ts import { App } from '@slack/bolt'; const app = new App({ signingSecret: process.env.SLACK_SIGNING_SECRET, clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, scopes: [ "channels:manage", "channels:read", "chat:write", "groups:read", "incoming-webhook", ], installerOptions: { authVersion: "v2", directInstall: false, installPath: "/slack/install", metadata: "", redirectUriPath: "/slack/oauth_redirect", stateVerification: "true", /** * Example pages to navigate to on certain callbacks. */ callbackOptions: { success: (installation, installUrlOptions, req, res) => { res.send("The installation succeeded!"); }, failure: (error, installUrlOptions, req, res) => { res.send("Something strange happened..."); }, }, /** * Example validation of installation options using a random state and an * expiration time between requests. */ stateStore: { generateStateParam: async (installUrlOptions, now) => { const state = randomStringGenerator(); const value = { options: installUrlOptions, now: now.toJSON() }; await database.set(state, value); return state; }, verifyStateParam: async (now, state) => { const value = await database.get(state); const generated = new Date(value.now); const seconds = Math.floor( (now.getTime() - generated.getTime()) / 1000, ); if (seconds > 600) { throw new Error("The state expired after 10 minutes!"); } return value.options; }, }, }, }); // App starts local server with route for /slack/events (async () => { await app.start(); })(); ``` ```ts import { App } from '@microsoft/teams.apps'; // Define app const app = new App({ clientId: process.env.ENTRA_APP_CLIENT_ID!, clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!, tenantId: process.env.ENTRA_TENANT_ID!, }); // App starts local server with route for /api/messages // To reuse your restify or other server, // create a custom `HttpPlugin`. (async () => { await app.start(); })(); ``` ```ts // triggers user sends "hi" or "@bot hi" // highlight-error-start app.message("hi", async ({ message, say }) => { // Handle only newly posted messages here if (message.subtype) return; await say(`Hello, <@${message.user}>`); }); // highlight-error-end // highlight-success-start app.message("hi", async ({ send, activity }) => { await send(`Hello, ${activity.from.name}!`); }); // highlight-success-end // listen for ANY message to be received // highlight-error-start app.message(async ({ message, say }) => { // Handle only newly posted messages here if (message.subtype) return; // echo back users request await say(`you said: ${message.text}`); }); // highlight-error-end // highlight-success-start app.on('message', async ({ send, activity }) => { // echo back users request await send(`you said: ${activity.text}`); }); // highlight-success-end ``` ```ts // triggers when user sends a message containing "hi" app.message("hi", async ({ message, say }) => { // Handle only newly posted messages here if (message.subtype) return; await say(`Hello, <@${message.user}>`); }); // listen for ANY message app.message(async ({ message, say }) => { // Handle only newly posted messages here if (message.subtype) return; // echo back users request await say(`you said: ${message.text}`); }); ``` ```ts // triggers when user sends "hi" or "@bot hi" app.message("hi", async ({ send, activity }) => { await send(`Hello, ${activity.from.name}!`); }); // listen for ANY message to be received app.on('message', async ({ send, activity }) => { // echo back users request await send(`you said: ${activity.text}`); }); ``` ```ts // highlight-error-start app.message('card', async (client) => { await say({ blocks: [ { type: 'section', text: { type: 'plain_text', text: 'Hello, world!', }, }, ], }); }); // highlight-error-end // highlight-success-start import { Card, TextBlock } from '@microsoft/teams.cards'; app.message('/card', async ({ send }) => { await send( new Card(new TextBlock('Hello, world!', { wrap: true, isSubtle: false })) .withOptions({ width: 'Full', }) ); }); // highlight-success-end ``` For existing cards like this, the simplest way to convert that to Teams SDK is this: ```ts app.message('card', async (client) => { await say({ blocks: [ { type: 'section', text: { type: 'plain_text', text: 'Hello, world!', }, }, ], }); }); ``` For a more thorough port, you could also do the following: ```ts import { Card, TextBlock } from '@microsoft/teams.cards'; app.message('/card', async ({ send }) => { await send( new Card(new TextBlock('Hello, world!', { wrap: true, isSubtle: false })).withOptions({ width: 'Full', }) ); }); ``` ```ts // highlight-error-start // TODO: Configure App class with user OAuth permissions and install app for user app.message('me', async ({ client, message }) => { const me = await client.users.info({ user: message.user }); await client.send(JSON.stringify(me)); }); // highlight-error-end // highlight-success-start import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ // ... rest of App config oauth: { // The key here should match the OAuth Connection setting // defined in your Azure Bot resource. defaultConnectionName: 'graph', }, }); app.message('me', async ({ signin, userGraph, send }) => { if (!await signin()) { return; } const me = await userGraph.call(endpoints.me.get); await send(JSON.stringify(me)); }); // highlight-success-end ``` ```ts // TODO: Configure App class with user OAuth permissions and install app for user app.message('me', async ({ client, message }) => { const me = await client.users.info({ user: message.user }); await client.send(JSON.stringify(me)); }); ``` ```ts import { App } from '@microsoft/teams.apps'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App({ // ... rest of App config oauth: { // The key here should match the OAuth Connection setting // defined in your Azure Bot resource. defaultConnectionName: 'graph', }, }); app.message('me', async ({ signin, userGraph, send }) => { if (!await signin()) { return; } const me = await userGraph.call(endpoints.me.get); await send(JSON.stringify(me)); }); ``` ```ts import { App } from '@microsoft/teams.apps'; const app = new App({ // ... rest of App config oauth: { // The key here should match the OAuth Connection setting // defined in your Azure Bot resource. defaultConnectionName: 'custom', }, }); app.message('me', async ({ activity, signin, token, send }) => { // In production, it is probably better to implement a local cache. // (e.g. \`activity.from.id\` <-> token). // Otherwise this triggers an API call to Azure Token Service on every inbound message. if (!await signin()) { return; } // Call external API const response = await fetch('https://example.com/api/helloworld', { method: 'POST', headers: { "Authorization": token, }, }); const result = await response.json(); await send(JSON.stringify(result)); }); ``` ================================================ FILE: teams.md/src/components/include/migrations/v1/python.incl.md ================================================ We'll also discuss how you can migrate features over incrementally via the [botbuilder plugin](./botbuilder). First, let's install Teams SDK into your project. Notably, this won't replace any existing installation of Teams SDK. When you've completed your migration, you can safely remove the `teams-ai` dependency from your `pyproject.toml` file. ```sh pip install microsoft-teams-apps ``` ```python # highlight-error-start - # in api.py - from http import HTTPStatus - - from aiohttp import web - from botbuilder.core.integration import aiohttp_error_middleware - - from bot import app - - routes = web.RouteTableDef() - - @routes.post("/api/messages") - async def on_messages(req: web.Request) -> web.Response: - res = await app.process(req) - if res is not None: - return res - return web.Response(status=HTTPStatus.OK) - - # in bot.py - import sys - import traceback - - from botbuilder.core import TurnContext, MemoryStorage - from teams import Application, ApplicationOptions, TeamsAdapter - from teams.state import TurnState - - config = Config() - storage = MemoryStorage() - app = Application[TurnState]( - ApplicationOptions( - bot_app_id=config.APP_ID, - adapter=TeamsAdapter(config), - storage=storage - ) - ) - - @app.activity("message") - async def on_message(context: TurnContext, _state: TurnState): - await context.send_activity(f"you said: {context.activity.text}") - return True - - @app.error - async def on_error(context: TurnContext, error: Exception): - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - traceback.print_exc() - await context.send_activity("The bot encountered an error or bug.") # highlight-error-end # highlight-success-start + # in main.py + import asyncio + import logging + + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App, ErrorEvent + from microsoft_teams.common import LocalStorage + + logger = logging.getLogger(__name__) + + # Define the app + app = App() + + # Optionally create local storage + storage: LocalStorage[str] = LocalStorage() + + @app.on_message + async def handle_message(ctx: ActivityContext[MessageActivity]): + await ctx.send(f"You said '{ctx.activity.text}'") + + # Listen for errors + @app.event("error") + async def handle_error(event: ErrorEvent) -> None: + """Handle errors.""" + logger.error(f"Error occurred: {event.error}") + if event.context: + logger.warning(f"Context: {event.context}") + + if __name__ == "__main__": + asyncio.run(app.start()) # highlight-success-end ``` ```python # in main.py import asyncio import logging from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext, App, ErrorEvent from microsoft_teams.common import LocalStorage logger = logging.getLogger(__name__) # Define the app app = App() # Optionally create local storage storage: LocalStorage[str] = LocalStorage() @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.send(f"You said '{ctx.activity.text}'") # Listen for errors @app.event("error") async def handle_error(event: ErrorEvent) -> None: """Handle errors.""" logger.error(f"Error occurred: {event.error}") if event.context: logger.warning(f"Context: {event.context}") if __name__ == "__main__": asyncio.run(app.start()) ``` ```python # in api.py from http import HTTPStatus from aiohttp import web from botbuilder.core.integration import aiohttp_error_middleware from bot import app routes = web.RouteTableDef() @routes.post("/api/messages") async def on_messages(req: web.Request) -> web.Response: res = await app.process(req) if res is not None: return res return web.Response(status=HTTPStatus.OK) api = web.Application(middlewares=[aiohttp_error_middleware]) api.add_routes(routes) # in app.py from aiohttp import web from api import api from config import Config if __name__ == "__main__": web.run_app(api, host="localhost", port=Config.PORT) # in bot.py import sys import traceback from botbuilder.core import TurnContext, MemoryStorage from teams import Application, ApplicationOptions, TeamsAdapter from teams.state import TurnState from config import Config config = Config() storage = MemoryStorage() app = Application[TurnState]( ApplicationOptions( bot_app_id=config.APP_ID, adapter=TeamsAdapter(config), storage=storage ) ) @app.activity("message") async def on_message(context: TurnContext, _state: TurnState): await context.send_activity(f"you said: {context.activity.text}") return True @app.error async def on_error(context: TurnContext, error: Exception): # This check writes out errors to console log .vs. app insights. # NOTE: In production environment, you should consider logging this to Azure # application insights. print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) traceback.print_exc() # Send a message to the user await context.send_activity("The bot encountered an error or bug.") ``` slightly ```python # highlight-error-start - # Triggered when user sends "hi" - @app.message(re.compile(r"hi", re.IGNORECASE)) - async def greeting(context: TurnContext, _state: AppTurnState) -> bool: - await context.send_activity("Hi there!") - return True - - # Listens for ANY message received - @app.activity("message") - async def on_message(context: TurnContext, _state: TurnState): - # Echoes back what user said - await context.send_activity(f"you said: {context.activity.text}") - return True # highlight-error-end # highlight-success-start + # Triggered when user sends "hi", "hello", or "greetings" + @app.on_message_pattern(re.compile(r"hello|hi|greetings")) + async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None: + await ctx.reply("Hello! How can I assist you today?") + + # Listens for ANY message received + @app.on_message + async def handle_message(ctx: ActivityContext[MessageActivity]): + # Sends a typing indicator + await ctx.reply(TypingActivityInput()) + await ctx.send(f"You said '{ctx.activity.text}'") # highlight-success-end ``` ```python # Triggered when user sends "hi", "hello", or "greetings" @app.on_message_pattern(re.compile(r"hello|hi|greetings")) async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None: await ctx.reply("Hello! How can I assist you today?") # Listens for ANY message received @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): # Sends a typing indicator await ctx.reply(TypingActivityInput()) await ctx.send(f"You said '{ctx.activity.text}'") ``` ```python # Triggered when user sends "hi" @app.message(re.compile(r"hi", re.IGNORECASE)) async def greeting(context: TurnContext, _state: AppTurnState) -> bool: await context.send_activity("Hi there!") return True # Listens for ANY message received @app.activity("message") async def on_message(context: TurnContext, _state: TurnState): # Echoes back what user said await context.send_activity(f"you said: {context.activity.text}") return True ``` Note that on Microsoft Teams, task modules have been renamed to dialogs. ```python # highlight-error-start - @app.task_module.fetch("connect-account") - async def on_connect_account(context: TurnContext, _state: TurnState): - return TaskModuleTaskInfo( - title="Connect your Microsoft 365 account", - height="medium", - width="medium", - url=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections", - fallbackUrl=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections", - completionBotId=config.NEXT_PUBLIC_BOT_ID, - ) - - @app.task_modules.submit("connect-account") - async def on_submit_connect_account(context: TurnContext, _state: TurnState, data: Dict[str, Any]): - print(json.dumps(data)) - await context.send_activity("You are all set! Now, how can I help you today?") - return None # highlight-error-end # highlight-success-start + @app.on_dialog_open + async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): + data: Optional[Any] = ctx.activity.value.data + dialog_type = data.get("opendialogtype") if data else None + + if dialog_type == "some_type": + return InvokeResponse( + body=TaskModuleResponse( + task=TaskModuleContinueResponse( + value=UrlTaskModuleTaskInfo( + title="Dialog title", + height="medium", + width="medium", + url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/some-path", + fallback_url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/fallback-path-for-web", + completion_bot_id= os.getenv("ENTRA_APP_CLIENT_ID"), + ) + ) + ) + ) + + @app.on_dialog_submit + async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): + data: Optional[Any] = ctx.activity.value.data + dialog_type = data.get("submissiondialogtype") if data else None + + if dialog_type == "some_type": + await ctx.send(json.dumps(ctx.activity.value)) + + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Received submit")) # highlight-success-end ``` ```python @app.on_dialog_open async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): data: Optional[Any] = ctx.activity.value.data dialog_type = data.get("opendialogtype") if data else None if dialog_type == "some_type": return InvokeResponse( body=TaskModuleResponse( task=TaskModuleContinueResponse( value=UrlTaskModuleTaskInfo( title="Dialog title", height="medium", width="medium", url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/some-path", fallback_url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/fallback-path-for-web", completion_bot_id= os.getenv("ENTRA_APP_CLIENT_ID"), ) ) ) ) @app.on_dialog_submit async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): data: Optional[Any] = ctx.activity.value.data dialog_type = data.get("submissiondialogtype") if data else None if dialog_type == "some_type": await ctx.send(json.dumps(ctx.activity.value)) return TaskModuleResponse(task=TaskModuleMessageResponse(value="Received submit")) ``` ```python @app.task_module.fetch("connect-account") async def on_connect_account(context: TurnContext, _state: TurnState): return TaskModuleTaskInfo( title="Connect your Microsoft 365 account", height="medium", width="medium", url=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections", fallbackUrl=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections", completionBotId=config.NEXT_PUBLIC_BOT_ID, ) @app.task_modules.submit("connect-account") async def on_submit_connect_account(context: TurnContext, _state: TurnState, data: Dict[str, Any]): print(json.dumps(data)) await context.send_activity("You are all set! Now, how can I help you today?") return None ``` ```python # highlight-error-start - @app.message("/card") - async def adaptive_card(context: TurnContext, _state: AppTurnState) -> bool: - attachment = CardFactory.adaptive_card( - { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.6", - "type": "AdaptiveCard", - "body": [ - { - "text": "Hello, world!", - "wrap": True, - "type": "TextBlock", - }, - ], - "msteams": { - "width": "Full" - } - } - ) - await context.send_activity(Activity(attachments=[attachment])) - return True # highlight-error-end # highlight-success-start + @app.on_message_pattern("/card") + async def handle_card_message(ctx: ActivityContext[MessageActivity]): + print(f"[CARD] Card requested by: {ctx.activity.from_}") + card = AdaptiveCard.model_validate( + { + "schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "type": "AdaptiveCard", + "body": [ + { + "text": "Hello, world!", + "wrap": True, + "type": "TextBlock", + }, + ], + "msteams": { + "width": "Full" + } + } + ) + await ctx.send(card) # highlight-success-end ``` For existing cards like this, the simplest way to convert that to Teams SDK is this: ```python @app.on_message_pattern("/card") async def handle_card_message(ctx: ActivityContext[MessageActivity]): print(f"[CARD] Card requested by: {ctx.activity.from_}") card = AdaptiveCard.model_validate( { "schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "type": "AdaptiveCard", "body": [ { "text": "Hello, world!", "wrap": True, "type": "TextBlock", }, ], "msteams": { "width": "Full" } } ) await ctx.send(card) ``` For a more thorough port, you could also do the following: ```python @app.on_message_pattern("/card") async def handle_card_message(ctx: ActivityContext[MessageActivity]): card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ TextBlock(text="Hello, world", wrap=True, weight="Bolder"), ], ms_teams=TeamsCardProperties(width='full'), ) await ctx.send(card) ``` ```python @app.message("/card") async def adaptive_card(context: TurnContext, _state: AppTurnState) -> bool: attachment = CardFactory.adaptive_card( { "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "type": "AdaptiveCard", "body": [ { "text": "Hello, world!", "wrap": True, "type": "TextBlock", }, ], "msteams": { "width": "Full" } } ) await context.send_activity(Activity(attachments=[attachment])) return True ``` ```python # highlight-error-start - app = Application[TurnState[ConversationState, UserState, TempState]]( - ApplicationOptions( - bot_app_id=config.APP_ID, - storage=MemoryStorage(), - adapter=TeamsAdapter(config), - auth=AuthOptions( - default="graph", - auto=True, - settings={ - "graph": OAuthOptions( - connection_name=config.OAUTH_CONNECTION_NAME, - title="Sign In", - text="please sign in", - end_on_invalid_message=True, - enable_sso=True, - ), - }, - ), - ) - ) - - auth = app.auth.get("graph") - - @app.message("/signout") - async def on_sign_out( - context: TurnContext, state: TurnState[ConversationState, UserState, TempState] - ): - await auth.sign_out(context, state) - await context.send_activity("you are now signed out...👋") - return False - - @auth.on_sign_in_success - async def on_sign_in_success( - context: TurnContext, state: TurnState[ConversationState, UserState, TempState] - ): - await context.send_activity("successfully logged in!") - await context.send_activity(f"token: {state.temp.auth_tokens['graph']}") - - @auth.on_sign_in_failure - async def on_sign_in_failure( - context: TurnContext, - _state: TurnState[ConversationState, UserState, TempState], - _res: SignInResponse, - ): - await context.send_activity("failed to login...") # highlight-error-end # highlight-success-start + app = App() + + @app.on_message + async def handle_message(ctx: ActivityContext[MessageActivity]): + ctx.logger.info("User requested sign-in.") + if ctx.is_signed_in: + await ctx.send("You are already signed in.") + else: + await ctx.sign_in() + + @app.on_message_pattern("/signout") + async def handle_sign_out(ctx: ActivityContext[MessageActivity]): + await ctx.sign_out() + await ctx.send("You have been signed out.") + + @app.event("sign_in") + async def handle_sign_in(event: SignInEvent): + """Handle sign-in events.""" + await event.activity_ctx.send("You are now signed in!") + + @app.event("error") + async def handle_error(event: ErrorEvent): + """Handle error events.""" + print(f"Error occurred: {event.error}") + if event.context: + print(f"Context: {event.context}") # highlight-success-end ``` ```python app = App() @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): ctx.logger.info("User requested sign-in.") if ctx.is_signed_in: await ctx.send("You are already signed in.") else: await ctx.sign_in() @app.on_message_pattern("/signout") async def handle_sign_out(ctx: ActivityContext[MessageActivity]): await ctx.sign_out() await ctx.send("You have been signed out.") @app.event("sign_in") async def handle_sign_in(event: SignInEvent): """Handle sign-in events.""" await event.activity_ctx.send("You are now signed in!") @app.event("error") async def handle_error(event: ErrorEvent): """Handle error events.""" print(f"Error occurred: {event.error}") if event.context: print(f"Context: {event.context}") ``` ```python app = Application[TurnState[ConversationState, UserState, TempState]]( ApplicationOptions( bot_app_id=config.APP_ID, storage=MemoryStorage(), adapter=TeamsAdapter(config), auth=AuthOptions( default="graph", auto=True, settings={ "graph": OAuthOptions( connection_name=config.OAUTH_CONNECTION_NAME, title="Sign In", text="please sign in", end_on_invalid_message=True, enable_sso=True, ), }, ), ) ) auth = app.auth.get("graph") @app.message("/signout") async def on_sign_out( context: TurnContext, state: TurnState[ConversationState, UserState, TempState] ): await auth.sign_out(context, state) await context.send_activity("you are now signed out...👋") return False @auth.on_sign_in_success async def on_sign_in_success( context: TurnContext, state: TurnState[ConversationState, UserState, TempState] ): await context.send_activity("successfully logged in!") await context.send_activity(f"token: {state.temp.auth_tokens['graph']}") @auth.on_sign_in_failure async def on_sign_in_failure( context: TurnContext, _state: TurnState[ConversationState, UserState, TempState], _res: SignInResponse, ): await context.send_activity("failed to login...") ``` ### Action planner When we created Teams SDK, LLM's didn't natively support tool calling or orchestration. A lot has changed since then, which is why we decided to deprecate `ActionPlanner` from Teams SDK, and replace it with something a bit more lightweight. Notably, Teams SDK had two similar concepts: functions and actions. In Teams SDK, these are consolidated into functions. ```python # highlight-error-start - # Create AI components - model = OpenAIModel( - OpenAIModelOptions(api_key=config.OPENAI_KEY, default_model="gpt-4o") - ) - - prompts = PromptManager( - PromptManagerOptions(prompts_folder=f"{os.path.dirname(__file__)}/prompts") - ) - - # Define a prompt function for getting the current status of the lights - @prompts.function("get_light_status") - async def on_get_light_status(context, state, functions, tokenizer, args): - return "on" if state.get("conversation.lightsOn") else "off" - - planner = ActionPlanner( - ActionPlannerOptions(model=model, prompts=prompts, default_prompt="tools") - ) - - # Define storage and application - storage = MemoryStorage() - app = Application[AppTurnState]( - ApplicationOptions( - bot_app_id=config.APP_ID, - storage=storage, - adapter=TeamsAdapter(config), - ai=AIOptions(planner=planner), - ) - ) - - # Register action handlers - @app.ai.action("LightsOn") - async def on_lights_on(context: ActionTurnContext, state: AppTurnState): - state.conversation.lights_on = True - return "the lights are now on" - - @app.ai.action("LightsOff") - async def on_lights_off(context: ActionTurnContext, state: AppTurnState): - state.conversation.lights_on = False - return "the lights are now off" - - @app.ai.action("Pause") - async def on_pause(context: ActionTurnContext, state: AppTurnState): - time_ms = int(context.data["time"]) if context.data["time"] else 1000 - time.sleep(time_ms / 1000) - return "done pausing" # highlight-error-end # highlight-success-start + import asyncio + from microsoft_teams.ai import ChatPrompt + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App + from microsoft_teams.common import LocalStorage + from microsoft_teams.openai import OpenAICompletionsAIModel + + storage = LocalStorage() + app = App() + + @app.on_message + async def handle_message(ctx: ActivityContext[MessageActivity]): + state = storage.get(ctx.activity.from_.id) or {"lights_on": False} + + prompt = ChatPrompt( + instructions="You are a helpful assistant that can control lights.", + model=OpenAICompletionsAIModel(model="gpt-4o"), + ) + + # Define functions inline + prompt.function( + "get_light_status", + "Gets the current status of the lights", + lambda: "on" if state["lights_on"] else "off" + ) + + prompt.function( + "lights_on", + "Turns the lights on", + lambda: (state.update({"lights_on": True}), "the lights are now on")[1] + ) + + prompt.function( + "lights_off", + "Turns the lights off", + lambda: (state.update({"lights_on": False}), "the lights are now off")[1] + ) + + async def pause(time: int): + await asyncio.sleep(time / 1000) + return "done pausing" + + prompt.function( + "pause", + "Delays for a period of time", + { + "type": "object", + "properties": { + "time": { + "type": "number", + "description": "The amount of time to delay in milliseconds" + } + }, + "required": ["time"] + }, + pause + ) + + result = await prompt.send(ctx.activity.text) + storage.set(ctx.activity.from_.id, state) + + if result.response.content: + await ctx.send(result.response.content) # highlight-success-end ``` In Teams SDK, there is no `actions.json` file. Instead, function prompts, parameters, etc. are declared in your code. ```python import asyncio from microsoft_teams.ai import ChatPrompt from microsoft_teams.api import MessageActivity from microsoft_teams.apps import ActivityContext, App from microsoft_teams.common import LocalStorage from microsoft_teams.openai import OpenAICompletionsAIModel storage = LocalStorage() app = App() @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): state = storage.get(ctx.activity.from_.id) or {"lights_on": False} prompt = ChatPrompt( instructions="You are a helpful assistant that can control lights.", model=OpenAICompletionsAIModel(model="gpt-4o"), ) # Define functions inline - no separate actions.json needed prompt.function( "get_light_status", "Gets the current status of the lights", lambda: "on" if state["lights_on"] else "off" ) prompt.function( "lights_on", "Turns the lights on", lambda: (state.update({"lights_on": True}), "the lights are now on")[1] ) prompt.function( "lights_off", "Turns the lights off", lambda: (state.update({"lights_on": False}), "the lights are now off")[1] ) async def pause(time: int): await asyncio.sleep(time / 1000) return "done pausing" prompt.function( "pause", "Delays for a period of time", { "type": "object", "properties": { "time": { "type": "number", "description": "The amount of time to delay in milliseconds" } }, "required": ["time"] }, pause ) result = await prompt.send(ctx.activity.text) storage.set(ctx.activity.from_.id, state) if result.response.content: await ctx.send(result.response.content) ``` ```python # Create AI components model = OpenAIModel( OpenAIModelOptions(api_key=config.OPENAI_KEY, default_model="gpt-4o") ) prompts = PromptManager( PromptManagerOptions(prompts_folder=f"{os.path.dirname(__file__)}/prompts") ) # Define a prompt function for getting the current status of the lights @prompts.function("get_light_status") async def on_get_light_status(context, state, functions, tokenizer, args): return "on" if state.get("conversation.lightsOn") else "off" planner = ActionPlanner( ActionPlannerOptions(model=model, prompts=prompts, default_prompt="tools") ) # Define storage and application storage = MemoryStorage() app = Application[AppTurnState]( ApplicationOptions( bot_app_id=config.APP_ID, storage=storage, adapter=TeamsAdapter(config), ai=AIOptions(planner=planner), ) ) # Register action handlers @app.ai.action("LightsOn") async def on_lights_on(context: ActionTurnContext, state: AppTurnState): state.conversation.lights_on = True return "the lights are now on" @app.ai.action("LightsOff") async def on_lights_off(context: ActionTurnContext, state: AppTurnState): state.conversation.lights_on = False return "the lights are now off" @app.ai.action("Pause") async def on_pause(context: ActionTurnContext, state: AppTurnState): time_ms = int(context.data["time"]) if context.data["time"] else 1000 time.sleep(time_ms / 1000) return "done pausing" @app.ai.action("LightStatus") async def on_lights_status(context: ActionTurnContext, state: AppTurnState): return "the lights are on" if state.conversation.lights_on else "the lights are off" ``` And the corresponding `actions.json` file: ```json [ { "name": "LightsOn", "description": "Turns on the lights" }, { "name": "LightsOff", "description": "Turns off the lights" }, { "name": "Pause", "description": "Delays for a period of time", "parameters": { "type": "object", "properties": { "time": { "type": "number", "description": "The amount of time to delay in milliseconds" } }, "required": ["time"] } }, { "name": "LightStatus", "description": "Gets the lights status" } ] ``` ```python # highlight-error-start - app = Application[AppTurnState]( - ApplicationOptions( - # ... other options - ai=AIOptions( - enable_feedback_loop=enableFeedbackLoop - ), - ) - ) - - @app.message() - async def on_message(context: TurnContext, state: AppTurnState): - await context.send_activity(Activity(text="Hey, give me feedback!", channel_data={"feedbackLoop": { "type": "custom"}})) - - @app.feedback_loop() - async def feedback_loop(context: TurnContext, state: AppTurnState, feedback_data: FeedbackLoopData): - print("Feedback loop triggered") # highlight-error-end # highlight-success-start + # Reply with message including feedback buttons + @app.on_message + async def handle_feedback(ctx: ActivityContext[MessageActivity]): + await ctx.send(MessageActivityInput(text="Hey, give me feedback!").add_ai_generated().add_feedback()) + + @app.on_message_submit_feedback + async def handle_message_feedback(ctx: ActivityContext[MessageSubmitActionInvokeActivity]): + # Custom logic here.. # highlight-success-end ``` ```python # Reply with message including feedback buttons @app.on_message async def handle_feedback(ctx: ActivityContext[MessageActivity]): await ctx.send(MessageActivityInput(text="Hey, give me feedback!").add_ai_generated().add_feedback()) @app.on_message_submit_feedback async def handle_message_feedback(ctx: ActivityContext[MessageSubmitActionInvokeActivity]): # Custom logic here.. ``` _Note:_ In Teams SDK, you do not need to opt into feedback at the `App` level. ```python app = Application[AppTurnState]( ApplicationOptions( # ... other options ai=AIOptions( enable_feedback_loop=enableFeedbackLoop ), ) ) @app.message() async def on_message(context: TurnContext, state: AppTurnState): await context.send_activity(Activity(text="Hey, give me feedback!", channel_data={"feedbackLoop": { "type": "custom"}})) @app.feedback_loop() async def feedback_loop(context: TurnContext, state: AppTurnState, feedback_data: FeedbackLoopData): print("Feedback loop triggered") ``` ## Incrementally migrating code via botbuilder plugin :::info Comparison code coming soon! ::: If you aren't ready to migrate all of your code, you can run your existing Teams SDK code in parallel with Teams SDK. Learn more [here](./botbuilder/integration). ================================================ FILE: teams.md/src/components/include/migrations/v1/typescript.incl.md ================================================ We'll also discuss how you can migrate features over incrementally via the [botbuilder plugin](./botbuilder). First, let's install Teams SDK into your project. Notably, this won't replace any existing installation of Teams SDK. When you've completed your migration, you can safely remove the `@microsoft/teams-ai` dependency from your `package.json` file. ```sh npm install @microsoft/teams.apps ``` ```ts // highlight-error-start - import { - ConfigurationServiceClientCredentialFactory, - MemoryStorage, - TurnContext, - } from 'botbuilder'; - import { Application, TeamsAdapter } from '@microsoft/teams-ai'; - import * as restify from 'restify'; // highlight-error-end // highlight-success-start + import { App } from '@microsoft/teams.apps'; + import { LocalStorage } from '@microsoft/teams.common/storage'; // highlight-success-end // highlight-error-start - // Create adapter. - const adapter = new TeamsAdapter( - {}, - new ConfigurationServiceClientCredentialFactory({ - MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID, - MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET, - MicrosoftAppType: 'SingleTenant', - MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID - }) - ); - // Catch-all for errors. - const onTurnErrorHandler = async (context: TurnContext, error: any) => { - console.error(`\n [onTurnError] unhandled error: ${error}`); - // Send a message to the user - await context.sendActivity('The bot encountered an error or bug.'); - }; - // Set the onTurnError for the singleton CloudAdapter. - adapter.onTurnError = onTurnErrorHandler; - // Create HTTP server. - const server = restify.createServer(); - server.use(restify.plugins.bodyParser()); - server.listen(process.env.port || process.env.PORT || 3978, () => { - console.log(`\n${server.name} listening to ${server.url}`); - }); - // Define storage and application - const app = new Application({ - storage: new MemoryStorage() - }); - // Listen for incoming server requests. - server.post('/api/messages', async (req, res) => { - // Route received a request to adapter for processing - await adapter.process(req, res, async (context) => { - // Dispatch to application for routing - await app.run(context); - }); - }); // highlight-error-end // highlight-success-start * // Define app * const app = new App({ * clientId: process.env.ENTRA_APP_CLIENT_ID!, * clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!, * tenantId: process.env.ENTRA_TENANT_ID!, * }); * // Optionally create local storage * const storage = new LocalStorage(); * // Listen for errors * app.event('error', async (client) => { * console.error('Error event received:', client.error); * if (client.activity) { * await app.send( * client.activity.conversation.id, * 'An error occurred while processing your message.', * ); * } * }); * // App creates local server with route for /api/messages * // To reuse your restify or other server, * // create a custom `HttpPlugin`. * (async () => { * // starts the server * await app.start(); * })(); // highlight-success-end ``` ```ts import { App } from '@microsoft/teams.apps'; import { LocalStorage } from '@microsoft/teams.common/storage'; // Define app const app = new App({ clientId: process.env.ENTRA_APP_CLIENT_ID!, clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!, tenantId: process.env.ENTRA_TENANT_ID!, }); // Optionally create local storage const storage = new LocalStorage(); // Listen for errors app.event('error', async (client) => { console.error('Error event received:', client.error); if (client.activity) { await app.send( client.activity.conversation.id, 'An error occurred while processing your message.', ); } }); // App creates local server with route for /api/messages // To reuse your restify or other server, // create a custom `HttpPlugin`. (async () => { // starts the server await app.start(); })(); ``` ```ts import { ConfigurationServiceClientCredentialFactory, MemoryStorage, TurnContext, } from 'botbuilder'; import { Application, TeamsAdapter } from '@microsoft/teams-ai'; import * as restify from 'restify'; // Create adapter. const adapter = new TeamsAdapter( {}, new ConfigurationServiceClientCredentialFactory({ MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID, MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET, MicrosoftAppType: 'SingleTenant', MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID }) ); // Catch-all for errors. const onTurnErrorHandler = async (context: TurnContext, error: any) => { console.error(`\n [onTurnError] unhandled error: ${error}`); // Send a message to the user await context.sendActivity('The bot encountered an error or bug.'); }; // Set the onTurnError for the singleton CloudAdapter. adapter.onTurnError = onTurnErrorHandler; // Create HTTP server. const server = restify.createServer(); server.use(restify.plugins.bodyParser()); server.listen(process.env.port || process.env.PORT || 3978, () => { console.log(`\n${server.name} listening to ${server.url}`); }); // Define storage and application const app = new Application({ storage: new MemoryStorage() }); // Listen for incoming server requests. server.post('/api/messages', async (req, res) => { // Route received a request to adapter for processing await adapter.process(req, res, async (context) => { // Dispatch to application for routing await app.run(context); }); }); ``` Activity handlers in Teams SDK v2 work slightly differently than in v1, with a more streamlined event-based approach. ```ts // triggers when user sends "/hi" or "@bot /hi" // highlight-error-start - app.message("/hi", async (context) => { - await context.sendActivity("Hi!"); - }); // highlight-error-end // highlight-success-start + app.message('/hi', async (client) => { + // SDK does not auto send typing indicators + await client.send({ type: 'typing' }); + await client.send("Hi!"); + }); // highlight-success-end // listen for ANY message to be received // highlight-error-start - app.activity( - ActivityTypes.Message, - async (context) => { - // echo back users request - await context.sendActivity( - `you said: ${context.activity.text}` - ); - } - ); // highlight-error-end // highlight-success-start + app.on('message', async (client) => { + await client.send({ type: 'typing' }); + await client.send( + `you said "${client.activity.text}"` + ); + }); // highlight-success-end ``` ```ts // triggers when user sends "/hi" or "@bot /hi" app.message("/hi", async (context) => { await context.sendActivity("Hi!"); }); // listen for ANY message to be received app.activity( ActivityTypes.Message, async (context) => { // echo back users request await context.sendActivity( `you said: ${context.activity.text}` ); } ); ``` ```ts // triggers when user sends "/hi" or "@bot /hi" app.message('/hi', async (client) => { // SDK does not auto send typing indicators await client.send({ type: 'typing' }); await client.send("Hi!"); }); // listen for ANY message to be received app.on('message', async (client) => { await client.send({ type: 'typing' }); await client.send( `you said "${client.activity.text}"` ); }); ``` Note that on Microsoft Teams, task modules have been renamed to dialogs. ```ts // highlight-error-start - app.taskModules.fetch('connect-account', async (context, state, data) => { - const taskInfo: TaskModuleTaskInfo = { - title: 'Connect your Microsoft 365 account', - height: 'medium', - width: 'medium', - url: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`, - fallbackUrl: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`, - completionBotId: process.env.NEXT_PUBLIC_BOT_ID, - }; - return taskInfo; - }); - app.taskModules.submit('connect-account', async (context, state, data) => { - console.log( - `bot-app.ts taskModules.submit("connect-account"): data`, - JSON.stringify(data, null, 4) - ); - await context.sendActivity('You are all set! Now, how can I help you today?'); - return undefined; - }); // highlight-error-end // highlight-success-start + app.on('dialog.open', (client) => { + const dialogType = client.activity.value.data?.opendialogtype; + if (dialogType === 'some-type') { + return { + task: { + type: 'continue', + value: { + title: 'Dialog title', + height: 'medium', + width: 'medium', + url: `https://${process.env.YOUR_WEBSITE_DOMAIN}/some-path`, + fallbackUrl: `https://${process.env.YOUR_WEBSITE_DOMAIN}/fallback-path-for-web`, + completionBotId: process.env.ENTRA_APP_CLIENT_ID!, + }, + }, + }; + } + }); - app.on('dialog.submit', async (client) => { - const dialogType = client.activity.value.data?.submissiondialogtype; - if (dialogType === 'some-type') { - const { data } = client.activity.value; - await client.send(JSON.stringify(data)); - } - return undefined; - }); // highlight-success-end ``` ```ts app.on('dialog.open', (client) => { const dialogType = client.activity.value.data?.opendialogtype; if (dialogType === 'some-type') { return { task: { type: 'continue', value: { title: 'Dialog title', height: 'medium', width: 'medium', url: `https://${process.env.YOUR_WEBSITE_DOMAIN}/some-path`, fallbackUrl: `https://${process.env.YOUR_WEBSITE_DOMAIN}/fallback-path-for-web`, completionBotId: process.env.ENTRA_APP_CLIENT_ID!, }, }, }; } }); app.on('dialog.submit', async (client) => { const dialogType = client.activity.value.data?.submissiondialogtype; if (dialogType === 'some-type') { const { data } = client.activity.value; await client.send(JSON.stringify(data)); } return undefined; }); ``` ```ts app.taskModules.fetch('connect-account', async (context, state, data) => { const taskInfo: TaskModuleTaskInfo = { title: 'Connect your Microsoft 365 account', height: 'medium', width: 'medium', url: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`, fallbackUrl: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`, completionBotId: process.env.NEXT_PUBLIC_BOT_ID, }; return taskInfo; }); app.taskModules.submit('connect-account', async (context, state, data) => { console.log( `bot-app.ts taskModules.submit("connect-account"): data`, JSON.stringify(data, null, 4) ); await context.sendActivity('You are all set! Now, how can I help you today?'); return undefined; }); ``` ```ts // highlight-error-start - app.message('/card', async (context: TurnContext) => { - const card = CardFactory.adaptiveCard({ - $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', - version: '1.5', - type: 'AdaptiveCard', - body: [ - { - type: 'TextBlock', - text: 'Hello, world!', - wrap: true, - isSubtle: false, - }, - ], - msteams: { - width: 'Full', - }, - }); - await context.sendActivity({ - attachments: [card], - }); - }); // highlight-error-end // highlight-success-start + app.message('/card', async (client) => { + await client.send({ + $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', + version: '1.5', + type: 'AdaptiveCard', + body: [ + { + type: 'TextBlock', + text: 'Hello, world!', + wrap: true, + isSubtle: false, + }, + ], + msteams: { + width: 'Full', + }, + }); + }); // highlight-success-end ``` For existing cards like this, the simplest way to convert that to Teams SDK is this: ```ts app.message('/card', async (client) => { await client.send({ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', version: '1.5', type: 'AdaptiveCard', body: [ { type: 'TextBlock', text: 'Hello, world!', wrap: true, isSubtle: false, }, ], msteams: { width: 'Full', }, }); }); ``` For a more thorough port, you could also do the following: ```ts import { Card, TextBlock } from '@microsoft/teams.cards'; app.message('/card', async (client) => { await client.send( new Card(new TextBlock('Hello, world!', { wrap: true, isSubtle: false })).withOptions({ width: 'Full', }) ); }); ``` ```ts app.message('/card', async (context: TurnContext) => { const card = CardFactory.adaptiveCard({ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', version: '1.5', type: 'AdaptiveCard', body: [ { type: 'TextBlock', text: 'Hello, world!', wrap: true, isSubtle: false, }, ], msteams: { width: 'Full', }, }); await context.sendActivity({ attachments: [card], }); }); ``` ```ts // highlight-error-start - const storage = new MemoryStorage(); - const app = new Application({ - storage: new MemoryStorage(), - authentication: { - autoSignIn: (context) => { - const activity = context.activity; - // No auth when user wants to sign in - if (activity.text === '/signout') { - return Promise.resolve(false); - } - // No auth for "/help" - if (activity.text === '/help') { - return Promise.resolve(false); - } - // Manually sign in (for illustrative purposes) - if (activity.text === '/signin') { - return Promise.resolve(false); - } - // For all other messages, require sign in - return Promise.resolve(true); - }, - settings: { - graph: { - connectionName: process.env.OAUTH_CONNECTION_NAME!, - title: 'Sign in', - text: 'Please sign in to use the bot.', - endOnInvalidMessage: true, - tokenExchangeUri: process.env.TOKEN_EXCHANGE_URI!, - enableSso: true, - }, - }, - }, - }); - - app.message('/signout', async (context, state) => { - await app.authentication.signOutUser(context, state); - await context.sendActivity(`You have signed out`); - }); - - app.message('/help', async (context, state) => { - await context.sendActivity(`your help text`); - }); - - app.authentication.get('graph').onUserSignInSuccess(async (context, state) => { - await context.sendActivity('Successfully logged in'); - await context.sendActivity(`Token string length: ${state.temp.authTokens['graph']!.length}`); - }); // highlight-error-end // highlight-success-start + const app = new App({ + oauth: { + defaultConnectionName: 'graph', + }, + logger: new ConsoleLogger('@tests/auth', { level: 'debug' }), + }); + + app.message('/signout', async ({ isSignedIn, signout, send }) => { + if (!isSignedIn) return; + await signout(); + await send('you have been signed out!'); + }); + + app.message('/help', async ({ send }) => { + await send('your help text'); + }); + + app.on('message', async ({ signin, userGraph, log }) => { + if (!await signin({ + oauthCardText: 'Sign in to your account', + signInButtonText: 'Sign in', + })) { + return; + } + const me = await userGraph.me.get(); + log.info(`user "${me.displayName}" already signed in!`); + }); + + app.event('signin', async ({ userGraph, send, token }) => { + const me = await userGraph.me.get(); + await send(`user "${me.displayName}" signed in.`); + await send(`Token string length: ${token.token.length}`); + }); // highlight-success-end ``` ```ts const app = new App({ oauth: { // oauth configurations /** * The name of the auth connection to use. * It should be the same as the OAuth connection name defined in the Azure Bot configuration. */ defaultConnectionName: 'graph', }, logger: new ConsoleLogger('@tests/auth', { level: 'debug' }), }); app.message('/signout', async ({ isSignedIn, signout, send }) => { if (!isSignedIn) return; await signout(); // call signout for your auth connection... await send('you have been signed out!'); }); app.message('/help', async ({ send }) => { await send('your help text'); }); app.on('message', async ({ signin, userGraph, log }) => { if (!await signin({ // Customize the OAuth card text (only renders in OAuth flow, not SSO) oauthCardText: 'Sign in to your account', signInButtonText: 'Sign in', })) { // call signin for your auth connection... return; } const me = await userGraph.me.get(); log.info(`user "${me.displayName}" already signed in!`); }); app.event('signin', async ({ userGraph, send, token }) => { const me = await userGraph.me.get(); await send(`user "${me.displayName}" signed in.`); await send(`Token string length: ${token.token.length}`); }); ``` ```ts const storage = new MemoryStorage(); const app = new Application({ storage: new MemoryStorage(), authentication: { autoSignIn: (context) => { const activity = context.activity; // No auth when user wants to sign in if (activity.text === '/signout') { return Promise.resolve(false); } // No auth for "/help" if (activity.text === '/help') { return Promise.resolve(false); } // Manually sign in (for illustrative purposes) if (activity.text === '/signin') { return Promise.resolve(false); } // For all other messages, require sign in return Promise.resolve(true); }, settings: { graph: { connectionName: process.env.OAUTH_CONNECTION_NAME!, title: 'Sign in', text: 'Please sign in to use the bot.', endOnInvalidMessage: true, tokenExchangeUri: process.env.TOKEN_EXCHANGE_URI!, enableSso: true, }, }, }, }); app.message('/signout', async (context, state) => { await app.authentication.signOutUser(context, state); // Echo back users request await context.sendActivity(`You have signed out`); }); app.message('/signin', async (context, state) => { let token = state.temp.authTokens['graph']; if (!token) { const res = await app.authentication.signInUser(context, state); if (res.error) { console.log(res.error); return; } token = state.temp.authTokens['graph']; } if (token) { // Echo back users request await context.sendActivity(`You are already authenticated!`); return; } // Sign in is pending... }); app.message('/help', async (context, state) => { await context.sendActivity(`your help text`); }); app.authentication.get('graph').onUserSignInSuccess(async (context, state) => { // Successfully logged in await context.sendActivity('Successfully logged in'); await context.sendActivity(`Token string length: ${state.temp.authTokens['graph']!.length}`); }); app.authentication.get('graph').onUserSignInFailure(async (context, _state, error) => { // Failed to login await context.sendActivity(`Failed to login with error: ${error.message}`); }); ``` ### Action planner When we created Teams SDK, LLM's didn't natively support tool calling or orchestration. A lot has changed since then, which is why we decided to deprecate `ActionPlanner` from Teams SDK, and replace it with something a bit more lightweight. Notably, Teams SDK had two similar concepts: functions and actions. In Teams SDK, these are consolidated into functions. ```ts // highlight-error-start - // Create AI components - const model = new OpenAIModel({ - apiKey: process.env.OPENAI_KEY!, - defaultModel: 'gpt-4o', - logRequests: true, - }); - - const prompts = new PromptManager({ - promptsFolder: path.join(__dirname, '../src/prompts'), - }); - - // Define a prompt function for getting the current status of the lights - prompts.addFunction('getLightStatus', async (context, memory) => { - return memory.getValue('conversation.lightsOn') ? 'on' : 'off'; - }); - - const planner = new ActionPlanner({ - model, - prompts, - defaultPrompt: 'tools', - }); - - // Define storage and application - const storage = new MemoryStorage(); - const app = new Application({ - storage, - ai: { - planner, - }, - }); - - // Register action handlers - app.ai.action('ToggleLights', async (context, state) => { - state.conversation.lightsOn = !state.conversation.lightsOn; - const lightStatusText = state.conversation.lightsOn ? 'on' : 'off'; - await context.sendActivity(`[lights ${lightStatusText}]`); - return `the lights are now ${lightStatusText}$`; - }); - - app.ai.action('Pause', async (context, state, parameters: PauseParameters) => { - await context.sendActivity(`[pausing for ${parameters.time / 1000} seconds]`); - await new Promise((resolve) => setTimeout(resolve, parameters.time)); - return `done pausing`; - }); // highlight-error-end // highlight-success-start + const storage = new LocalStorage(); + const app = new App(); + + app.on('message', async (client) => { + let state = storage.get(client.activity.from.id); + + if (!state) { + state = { + status: false, + messages: [], + }; + storage.set(client.activity.from.id, state); + } + + const prompt = new ChatPrompt({ + messages: state.messages, + instructions: `The assistant can turn a light on or off. The lights are currently off.`, + model: new OpenAIChatModel({ + model: 'gpt-4o-mini', + apiKey: process.env.OPENAI_API_KEY, + }), + }) + .function('get_light_status', 'get the current light status', () => { + return state.status; + }) + .function('toggle_lights', 'toggles the lights on/off', () => { + state.status = !state.status; + storage.set(client.activity.from.id, state); + }) + .function( + 'pause', + 'delays for a period of time', + { + type: 'object', + properties: { + time: { + type: 'number', + description: 'the amount of time to delay in milliseconds', + }, + }, + required: ['time'], + }, + async ({ time }: { time: number }) => { + await new Promise((resolve) => setTimeout(resolve, time)); + } + ); + + await prompt.send(client.activity.text, { + onChunk: (chunk) => { + client.stream.emit(new MessageActivity(chunk)); + }, + }); + }); // highlight-success-end ``` In Teams SDK, there is no `actions.json` file. Instead, function prompts, parameters, etc. are declared in your code. ```ts import '@azure/openai/types'; import { ChatPrompt, Message } from '@microsoft/teams.ai'; import { MessageActivity } from '@microsoft/teams.api'; import { App } from '@microsoft/teams.apps'; import { LocalStorage } from '@microsoft/teams.common/storage'; import { OpenAIChatModel } from '@microsoft/teams.openai'; interface IStorageState { status: boolean; messages: Message[]; } const storage = new LocalStorage(); const app = new App(); app.on('message', async (client) => { let state = storage.get(client.activity.from.id); if (!state) { state = { status: false, messages: [], }; storage.set(client.activity.from.id, state); } const prompt = new ChatPrompt({ messages: state.messages, instructions: `The following is a conversation with an AI assistant. The assistant can turn a light on or off. The lights are currently off.`, model: new OpenAIChatModel({ model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY, }), }) .function('get_light_status', 'get the current light status', () => { return state.status; }) .function('toggle_lights', 'toggles the lights on/off', () => { state.status = !state.status; storage.set(client.activity.from.id, state); }) .function( 'pause', 'delays for a period of time', { type: 'object', properties: { time: { type: 'number', description: 'the amount of time to delay in milliseconds', }, }, required: ['time'], }, async ({ time }: { time: number }) => { await new Promise((resolve) => setTimeout(resolve, time)); } ); await prompt.send(client.activity.text, { onChunk: (chunk) => { client.stream.emit(new MessageActivity(chunk)); }, }); }); (async () => { await app.start(); })(); ``` ```ts // Create AI components const model = new OpenAIModel({ // OpenAI Support apiKey: process.env.OPENAI_KEY!, defaultModel: 'gpt-4o', // Azure OpenAI Support azureApiKey: process.env.AZURE_OPENAI_KEY!, azureDefaultDeployment: 'gpt-4o', azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!, azureApiVersion: '2023-03-15-preview', // Request logging logRequests: true, }); const prompts = new PromptManager({ promptsFolder: path.join(__dirname, '../src/prompts'), }); // Define a prompt function for getting the current status of the lights prompts.addFunction('getLightStatus', async (context, memory) => { return memory.getValue('conversation.lightsOn') ? 'on' : 'off'; }); const planner = new ActionPlanner({ model, prompts, defaultPrompt: 'tools', }); // Define storage and application const storage = new MemoryStorage(); const app = new Application({ storage, ai: { planner, }, }); // Register action handlers app.ai.action('ToggleLights', async (context, state) => { state.conversation.lightsOn = !state.conversation.lightsOn; const lightStatusText = state.conversation.lightsOn ? 'on' : 'off'; await context.sendActivity(`[lights ${lightStatusText}]`); return `the lights are now ${lightStatusText}$`; }); interface PauseParameters { time: number; } app.ai.action('Pause', async (context, state, parameters: PauseParameters) => { await context.sendActivity(`[pausing for ${parameters.time / 1000} seconds]`); await new Promise((resolve) => setTimeout(resolve, parameters.time)); return `done pausing`; }); // Listen for incoming server requests. server.post('/api/messages', async (req, res) => { // Route received a request to adapter for processing await adapter.process(req, res as any, async (context) => { // Dispatch to application for routing await app.run(context); }); }); ``` And the corresponding `actions.json` file: ```json [ { "name": "ToggleLights", "description": "Turns on/off the lights" }, { "name": "Pause", "description": "Delays for a period of time", "parameters": { "type": "object", "properties": { "time": { "type": "number", "description": "The amount of time to delay in milliseconds" } }, "required": ["time"] } } ] ``` ```ts // highlight-error-start - export const app = new Application({ - ai: { - // opts into feedback loop - enable_feedback_loop: true, - }, - }); - - // Reply with message including feedback buttons - app.activity(ActivityTypes.Message, async (context) => { - await context.sendActivity({ - type: ActivityTypes.Message, - text: `Hey, give me feedback!`, - channelData: { - feedbackLoop: { - type: 'custom', - }, - }, - }); - }); - - // Handle feedback submit - app.feedbackLoop(async (context, state, feedbackLoopData) => { - // custom logic here... - }); // highlight-error-end // highlight-success-start + // Reply with message including feedback buttons + app.on('message', async (client) => { + await client.send( + new MessageActivity('Hey, give me feedback!') + .addAiGenerated() // AI generated label + .addFeedback() // Feedback buttons + ); + }); + + // Listen for feedback submissions + app.on('message.submit.feedback', async ({ activity, log }) => { + // custom logic here... + }); // highlight-success-end ``` ```ts import { MessageActivity } from '@microsoft/teams.api'; // Reply with message including feedback buttons app.on('message', async (client) => { await client.send( new MessageActivity('Hey, give me feedback!') .addAiGenerated() // AI generated label .addFeedback() // Feedback buttons ); }); // Listen for feedback submissions app.on('message.submit.feedback', async ({ activity, log }) => { // custom logic here... }); ``` _Note:_ In Teams SDK, you do not need to opt into feedback at the `App` level. ```ts export const app = new Application({ ai: { // opts into feedback loop enable_feedback_loop: true, }, }); // Reply with message including feedback buttons app.activity(ActivityTypes.Message, async (context) => { await context.sendActivity({ type: ActivityTypes.Message, text: `Hey, give me feedback!`, channelData: { feedbackLoop: { type: 'custom', }, }, }); }); // Handle feedback submit app.feedbackLoop(async (context, state, feedbackLoopData) => { // custom logic here... }); ``` ## Incrementally migrating code via botbuilder plugin :::info Comparison code coming soon! ::: If you aren't ready to migrate all of your code, you can run your existing Teams SDK code in parallel with Teams SDK. Learn more [here](./botbuilder/integration). ================================================ FILE: teams.md/src/components/include/migrations/v2-previews/typescript.incl.md ================================================ If you're moving from preview versions of Teams SDK, you may encounter a few breaking changes along the way. This page outlines those and shows how to get back on track. ## Graph Client The Graph Client has been redesigned to be more flexible and support far more Graph APIs, while being mindful of code and package size. The Graph endpoints are now an optional dependency, and one that is fully tree-shakable if you're using a modern bundler. The redesign changes the calling pattern slightly. If you were using Graph APIs, you may need to update your code. ### Installing endpoints dependency The first step in the migration is to install the `@microsoft/teams.graph-endpoints` dependency. This dependency is optional to reduce overhead for for applications that don't need to call Graph APIs. If you're reading this migration guide however, you'll likely want to install it. This package is installed just like any other NPM package, using your package manager of choice. For instance: ```sh npm install @microsoft/teams.graph-endpoints ``` ### Updating code Once the endpoints dependency is installed, the code changes should be fairly straight forward as the overall Graph taxonomy and API naming conventions have not changed. The overall change is that instead of invoking an endpoint function directly, you now pass in an endpoint to the `graphClient.call()` method. #### Calling endpoints In earlier preview versions, the way to get details for the current user was: ```typescript // GET /me const me = await app.graph.me.get(); ``` In current versions, the equivalent method is: ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; // GET /me const me = await app.graph.call(endpoints.me.get); ``` #### Providing arguments In earlier preview versions, variables were passes as an argument when invoking the endpoint. To get details for a specific user, you would do the following: ```typescript // GET /users/{id | userPrincipalName} const user = await app.graph.users.get({ 'user-id': userId }); ``` In current versions, the variables are provided as a separate argument after the endpoint: ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; // GET /users/{id | userPrincipalName} const user = await app.graph.call(endpoints.users.get, { 'user-id': userId }); ``` #### No more redundant arguments In earlier preview versions, some endpoints required the same argument to be provided twice. For instance: ```typescript // GET /teams/{team-id}/installedApps` const apps = await app.graph.teams.installedApps(teamId).list({ 'team-id': teamId }); ``` In current versions, once is enough: ```typescript // GET /teams/{team-id}/installedApps` const apps = await app.graph.call(endpoints.teams.installedApps.list, { 'team-id': teamId }); ``` #### Improving readability If you find it helpful for readability, you can scope your endpoint import as you prefer. For instance: ```typescript import * as endpoints from '@microsoft/teams.graph-endpoints'; import { me } from '@microsoft/teams.graph-endpoints'; import { presence } from '@microsoft/teams.graph-endpoints/me'; import { setPresence } from '@microsoft/teams.graph-endpoints/me/presence'; import { create as updatePresence } from '@microsoft/teams.graph-endpoints/me/presence/setPresence'; // different ways to POST to /me/presence/setPresence const newPresence = { availability: 'Away', activity: 'Away', sessionId: clientId }; await app.graph.call(endpoints.me.presence.setPresence.create, newPresence); await app.graph.call(me.presence.setPresence.create, newPresence); await app.graph.call(presence.setPresence.create, newPresence); await app.graph.call(setPresence.create, newPresence); await app.graph.call(updatePresence, newPresence); ``` ================================================ FILE: teams.md/src/constants/languages.ts ================================================ /** * Supported languages for Teams SDK documentation * This is the single source of truth for all language-related code */ export const LANGUAGES = ['typescript', 'csharp', 'python'] as const; export type Language = (typeof LANGUAGES)[number]; export const LANGUAGE_NAMES = { typescript: 'TypeScript', csharp: 'C#', python: 'Python', } as const; export const DEFAULT_LANGUAGE: Language = 'typescript'; /** * Maps page paths to arrays of languages where the page is NOT available * e.g. Typescript has activity-ref.md but C# and Python do not. */ export interface LanguageAvailabilityMap { [path: string]: Language[]; } ================================================ FILE: teams.md/src/css/code-blocks.css ================================================ .code-block-error-line { background-color: #ff000020; color: #ff000080 !important; display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); border-left: 3px solid #ff000080; * { color: #ff000080 !important; } } .code-block-success-line { background-color: #003100; color: #e6f6e6 !important; border-left: 3px solid #009400; display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); * { color: #e6f6e6 !important; } } [data-theme='dark'] { .code-block-error-line { background-color: #ff000020; color: rgba(254, 100, 100, 0.67) !important; border-left: 3px solid #ff000080; * { color:rgba(254, 100, 100, 0.67) !important; } } .code-block-success-line { background-color: #003100; color: #e6f6e6 !important; border-left: 3px solid #009400; * { color: #e6f6e6 !important; } } } ================================================ FILE: teams.md/src/css/custom.css ================================================ /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to * work well for content-centric websites. */ /* ========================================================================== CSS CUSTOM PROPERTIES / VARIABLES ========================================================================== */ /* You can override the default Infima variables here. */ :root { --ifm-color-primary: #3c47b7; --ifm-color-primary-dark: #3640a5; --ifm-color-primary-darker: #333c9c; --ifm-color-primary-darkest: #2a3280; --ifm-color-primary-light: #4853c3; --ifm-color-primary-lighter: #515cc6; --ifm-color-primary-lightest: #6d76cf; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); --navbar-background-color: rgb(222, 222, 222); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { --ifm-color-primary: #949cdc; --ifm-color-primary-dark: #7882d3; --ifm-color-primary-darker: #6a76ce; --ifm-color-primary-darkest: #414fc1; --ifm-color-primary-light: #b0b6e5; --ifm-color-primary-lighter: #bec2ea; --ifm-color-primary-lightest: #e7e9f7; --docusaurus-highlighted-code-line-bg: rgba(91, 91, 91, 0.3); --navbar-background-color: rgb(75, 75, 75); } /* ========================================================================== NAVBAR COMPONENTS ========================================================================== */ /* Navbar items styling */ .navbar__items:not(.navbar__items--right) .navbar__item { border: none; border-radius: 4px; padding: 8px 12px; text-decoration: none; display: inline-block; text-align: center; cursor: pointer; transition: background-color 0.3s ease; margin-right: 2px; &.navbar__link--active { background-color: var(--navbar-background-color); } } /* Color mode toggle spacing */ .navbar__items--right .colorModeToggle { margin-left: 0.5rem; } /* GitHub link icon */ .header-github-link:before { background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; content: ''; display: flex; height: 24px; width: 24px; } [data-theme='dark'] .header-github-link:before { background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; } /* ========================================================================== LANGUAGE DROPDOWN COMPONENT ========================================================================== */ /* Language dropdown container */ .language-dropdown { position: relative; display: inline-block; } /* Language dropdown trigger button */ .language-dropdown .navbar__link { display: flex; align-items: center; gap: 0.25rem; padding: 0.5rem 0.75rem; border: none; background: none; color: var(--ifm-navbar-link-color); text-decoration: none; cursor: pointer; font-size: var(--ifm-font-size-base); } .language-dropdown .navbar__link:hover { color: var(--ifm-navbar-link-hover-color); } .language-dropdown .navbar__link:focus { color: var(--ifm-navbar-link-hover-color); outline: 2px solid var(--ifm-focus-ring-color, var(--ifm-color-primary)); outline-offset: 2px; border-radius: 4px; } /* Language dropdown arrow */ .language-dropdown-arrow { margin-left: 0.5rem; width: 0; height: 0; border-style: solid; border-width: 6px 6px 0 6px; border-color: currentColor transparent transparent transparent; display: inline-block; vertical-align: middle; } /* Language dropdown menu */ #language-switch-list { position: absolute; top: 100%; left: 0; right: 0; min-width: 120px; background: var(--ifm-background-surface-color); border: 1px solid transparent; border-radius: var(--ifm-global-radius); box-shadow: var(--ifm-global-shadow-lw); list-style: none; margin: 0; padding: 0.5rem 0; z-index: var(--ifm-z-index-dropdown); } #language-switch-list li { margin: 0; padding: 0; } /* Language dropdown menu items */ #language-switch-list [role='option'], .language-dropdown-option { width: 100%; text-align: left; padding: 0.375rem 1rem; border: none; background: none; color: inherit; cursor: pointer; font-size: var(--ifm-font-size-base); display: block; transition: background-color var(--ifm-transition-fast); white-space: nowrap; -webkit-user-select: none; user-select: none; } #language-switch-list [role='option']:hover, #language-switch-list [role='option'][data-active='true'] { background-color: var(--ifm-menu-color-background-hover); } #language-switch-list:focus { border-radius: var(--ifm-global-radius); } /* Active language indicator */ #language-switch-list [role='option'][aria-selected='true'] { background-color: var(--ifm-color-emphasis-200); position: relative; } #language-switch-list [role='option'][aria-selected='true']::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background-color: var(--ifm-color-primary); } /* ========================================================================== CODE BLOCKS AND SYNTAX HIGHLIGHTING ========================================================================== */ /* Dark theme code background */ [data-theme='dark'] code { background-color: rgba(14, 13, 13, 1); } [data-theme='dark'] pre span { --ifm-pre-background: rgba(14, 13, 13, 1) !important; } /* ========================================================================== RESPONSIVE DESIGN ========================================================================== */ /* Simple language banner notification */ .language-banner { position: fixed; top: var(--ifm-navbar-height, 60px); left: 0; right: 0; width: 50%; margin: 0 auto; background: var(--ifm-color-info-contrast-background, #d1ecf1); color: var(--ifm-color-info-contrast-foreground, #0c5460); border: 1px solid var(--ifm-color-info); border-radius: var(--ifm-border-radius); padding: 0.75rem 1rem; font-size: 0.9rem; display: block; z-index: 1000; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); animation: slideInFromTop 0.3s ease-out; } .language-banner__content { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .language-banner__text { flex: 1; color: inherit; } .language-banner__actions { display: flex; align-items: center; gap: 0.5rem; } .language-banner__button { border: none; border-radius: var(--ifm-button-border-radius); padding: 0.25rem 0.75rem; font-size: 0.85rem; cursor: pointer; transition: all 0.2s ease; text-decoration: none; } .language-banner__button--primary { background: var(--ifm-color-primary); color: white; font-weight: 500; } .language-banner__button--primary:hover { background: var(--ifm-color-primary-dark); text-decoration: none; color: white; } .language-banner__button--secondary { background: transparent; color: var(--ifm-color-emphasis-600); padding: 0.25rem 0.5rem; font-size: 1.2rem; width: 1.5rem; height: 1.5rem; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .language-banner__button--secondary svg { width: 2rem; height: 2rem; } .language-banner__button--secondary:hover { background: var(--ifm-color-emphasis-200); color: var(--ifm-color-emphasis-800); } .language-banner__button:focus { outline: 2px solid var(--ifm-color-primary); outline-offset: 2px; } @keyframes slideInFromTop { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } /* Mobile responsiveness for LanguageDropdown */ @media (max-width: 768px) { .language-dropdown .navbar__link { padding: 0.4rem 0.6rem; font-size: 0.75rem; } #language-switch-list { min-width: 120px; } #language-switch-list [role='option'], .language-dropdown-option { padding: 0.5rem 0.75rem; white-space: nowrap; font-size: 0.8rem; } } ================================================ FILE: teams.md/src/hooks/useLanguagePreference.tsx ================================================ import React, { createContext, ReactNode, useState, useEffect, useContext, useCallback, useRef, } from 'react'; import { LANGUAGES, type Language, DEFAULT_LANGUAGE } from '../constants/languages'; interface LanguageContextProps { language: Language; setLanguage: (language: Language) => void; } const LanguageContext = createContext(undefined); const STORAGE_KEY = 'teams-sdk-language-preference'; // Type guard to check if a string is a valid Language const isLanguage = (value: string): value is Language => (LANGUAGES as readonly string[]).includes(value); // Verify localStorage is available and we can write to it. const isLocalStorageAvailable = (): boolean => { try { if (typeof window === 'undefined' || !('localStorage' in window)) { return false; } const testKey = '__test__'; localStorage.setItem(testKey, '1'); localStorage.removeItem(testKey); return true; } catch { return false; } }; export function LanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguage] = useState(() => { if (isLocalStorageAvailable()) { const stored = localStorage.getItem(STORAGE_KEY); if (stored && isLanguage(stored)) { return stored; } } return DEFAULT_LANGUAGE; }); const localStorageAvailable = useRef(isLocalStorageAvailable()); // Persist language storage across tabs useEffect(() => { if (!localStorageAvailable.current) { return; } const handler = (e: StorageEvent) => { if (e.key === STORAGE_KEY && e.newValue && isLanguage(e.newValue)) { setLanguage(e.newValue); } }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); }, []); const updateLanguage = useCallback((newLanguage: Language) => { setLanguage(newLanguage); if (localStorageAvailable.current) { localStorage.setItem(STORAGE_KEY, newLanguage); } }, []); return ( {children} ); } export function useLanguagePreference(): LanguageContextProps { const context = useContext(LanguageContext); if (!context) { throw new Error('useLanguagePreference must be used within a LanguageProvider'); } return context; } ================================================ FILE: teams.md/src/pages/csharp.tsx ================================================ import { Redirect } from '@docusaurus/router'; import useBaseUrl from '@docusaurus/useBaseUrl'; export default function CSharp() { const baseUrl = useBaseUrl('/'); return ; } ================================================ FILE: teams.md/src/pages/index.module.css ================================================ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; } @media screen and (max-width: 996px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } ================================================ FILE: teams.md/src/pages/index.tsx ================================================ import { Redirect } from '@docusaurus/router'; export default function Home() { return ; } ================================================ FILE: teams.md/src/pages/python.tsx ================================================ import { Redirect } from '@docusaurus/router'; import useBaseUrl from '@docusaurus/useBaseUrl'; export default function Python() { const baseUrl = useBaseUrl('/'); return ; } ================================================ FILE: teams.md/src/pages/templates/essentials/README.mdx ================================================ --- title: Essentials sidebar_position: 2 summary: Introduction to the core concepts of Teams SDK applications including events, activities, handlers, and the reactive paradigm for building intelligent agents. --- # Essentials At its core, an application that hosts an agent on Microsoft Teams exists to do three things well: listen to events, handle the ones that matter, and respond efficiently. Whether a user sends a message, opens a dialog, or clicks a button — your app is there to interpret the event and act on it. With Teams SDK, we've made it easier than ever to build this kind of reactive, conversational logic. The SDK introduces a few simple but powerful paradigms to help you connect to Teams, register handlers, and build intelligent agent behaviors quickly. Before diving in, let's define a few key terms: ```mermaid flowchart LR Teams["Teams"] Server["App Server"] AppEventHandlers[""] AppRouter["Activity Event Router"] AppActivityHandlers[""] Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server --> |ActivityEvent
or InvokeEvent| AppRouter Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers AppRouter --> |message activity| AppActivityHandlers AppRouter --> |card activity| AppActivityHandlers AppRouter --> |installation activity| AppActivityHandlers AppRouter --> |...other activities| AppActivityHandlers linkStyle 0,3 stroke:#66fdf3,stroke-width:1px,color:Tomato linkStyle 1,2,4,5 stroke:#66fdf3,stroke-width:1px linkStyle 6,7,8,9 color:Tomato ``` This section will walk you through the foundational pieces needed to build responsive, intelligent agents using the SDK. ================================================ FILE: teams.md/src/pages/templates/essentials/_category_.json ================================================ { "position": 2, "label": "Essentials", "collapsed": false } ================================================ FILE: teams.md/src/pages/templates/essentials/api.mdx ================================================ --- sidebar_position: 6 sidebar_label: API Client title: API Client summary: Overview of the Teams API Client and how to use it to interact with conversations, meetings, and teams in your application. --- # Teams API Client Teams has a number of areas that your application has access to via its API. These are all available via the object. Here is a short summary of the different areas: An instance of the API client is passed to handlers that can be used to fetch details: ## Example In this example, we use the API client to fetch the members in a conversation. The object is passed to the activity handler in this case. ## Proactive API It's also possible to access the API client from outside a handler via the app instance. Here we have the same example as above, but we're access the API client via the app instance. ## Meetings Example In this example, we use the API client to get a specific meeting participant's details, such as their role (e.g. Organizer) and whether they are currently in the meeting. Provide the user's AAD Object ID to specify which participant to look up. The `meetingId` and `tenantId` are available from the activity's channel data. :::note To retrieve **all** members of a meeting, use the conversations API as shown in the [example above](#example), since meetings are also conversations. ::: Visit [Meeting Events](../in-depth-guides/meeting-events) to learn more about meeting events. ================================================ FILE: teams.md/src/pages/templates/essentials/app-authentication.mdx ================================================ --- sidebar_position: 5 title: App Authentication summary: Configure app authentication in your Teams SDK application using client secrets, user managed identities, or federated identity credentials languages: ['typescript','python','csharp'] --- # App Authentication Your application needs to authenticate to send messages to Teams as your bot. Authentication allows your app service to certify that it is _allowed_ to send messages as your Azure Bot. :::info Azure Setup Required Before configuring your application, you must first set up authentication in Azure. See the [App Authentication Setup](/teams/app-authentication) guide for instructions on creating the necessary Azure resources. ::: ## Authentication Methods There are 3 main ways of authenticating: 1. **Client Secret** - Simple password-based authentication using a client secret 2. **User Managed Identity** - Passwordless authentication using Azure managed identities 3. **Federated Identity Credentials** - Advanced identity federation using managed identities ## Configuration Reference The Teams SDK automatically detects which authentication method to use based on the environment variables you set: | CLIENT_ID | CLIENT_SECRET | MANAGED_IDENTITY_CLIENT_ID | Authentication Method | |-|-|-|-| | not_set | | | No-Auth (local development only) | | set | set | | Client Secret | | set | not_set | | User Managed Identity | | set | not_set | set (same as CLIENT_ID) | User Managed Identity | | set | not_set | set (different from CLIENT_ID) | Federated Identity Credentials (UMI) | | set | not_set | "system" | Federated Identity Credentials (System Identity) | ## Client Secret The simplest authentication method using a password-like secret. ### Setup First, complete the [Client Secret Setup](/teams/app-authentication/client-secret) in Azure Portal or Azure CLI. ### Configuration Set the following environment variables in your application: - `CLIENT_ID`: Your Application (client) ID - `CLIENT_SECRET`: The client secret value you created - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-client-id-here CLIENT_SECRET=your-client-secret-here TENANT_ID=your-tenant-id ``` The SDK will automatically use Client Secret authentication when both `CLIENT_ID` and `CLIENT_SECRET` are provided. ## User Managed Identity Passwordless authentication using Azure managed identities - no secrets to rotate or manage. ### Setup First, complete the [User Managed Identity Setup](/teams/app-authentication/user-managed-identity) in Azure Portal or Azure CLI. ### Configuration ## Federated Identity Credentials Advanced identity federation allowing you to assign managed identities directly to your App Registration. ### Setup First, complete the [Federated Identity Credentials Setup](/teams/app-authentication/federated-identity-credentials) in Azure Portal or Azure CLI. ### Configuration Depending on the type of managed identity you select, set the environment variables accordingly. **For User Managed Identity:** Set the following environment variables: - `CLIENT_ID`: Your Application (client) ID - `MANAGED_IDENTITY_CLIENT_ID`: The Client ID for the User Managed Identity resource - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-app-client-id-here MANAGED_IDENTITY_CLIENT_ID=your-managed-identity-client-id-here # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` **For System Assigned Identity:** Set the following environment variables: - `CLIENT_ID`: Your Application (client) ID - `MANAGED_IDENTITY_CLIENT_ID`: `system` - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-app-client-id-here MANAGED_IDENTITY_CLIENT_ID=system # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` ## Troubleshooting If you encounter authentication errors, see the [Authentication Troubleshooting](/teams/app-authentication/troubleshooting) guide for common issues and solutions. ================================================ FILE: teams.md/src/pages/templates/essentials/app-basics.mdx ================================================ --- sidebar_position: 1 sidebar_label: App Basics title: App Basics summary: Comprehensive guide to the App class, the main entry point for Teams SDK agents that handles server hosting, request routing, authentication, and plugin management. suppressLanguageIncludeWarning: true --- # App Basics The `App` class is the main entry point for your agent. It is responsible for: 1. Hosting and running the server (via plugins) 2. Serving incoming requests and routing them to your handlers 3. Handling authentication for your agent to the Teams backend 4. Providing helpful utilities which simplify the ability for your application to interact with the Teams platform 5. Managing plugins which can extend the functionality of your agent ```mermaid flowchart LR %% Layout Definitions direction LR Teams subgraph AppClass CorePlugins["Plugins"] Events["Events"] subgraph AppResponsibilities direction TB ActivityRouting["Activity Routing"] Utilities["Utilities"] Auth["Auth"] end Plugins2["Plugins"] end ApplicationLogic["Application Logic"] %% Connections Teams --> CorePlugins CorePlugins --> Events Events --> ActivityRouting ActivityRouting --> Plugins2 Plugins2 --> ApplicationLogic Auth --> ApplicationLogic Utilities --> ApplicationLogic %% Styling style Teams fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style ApplicationLogic fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff ``` ## Core Components **Plugins** - Can be used to set up the server - Can listen to messages or send messages out **Events** - Listens to events from core plugins - Emit interesting events to the application **Activity Routing** - Routes activities to appropriate handlers **Utilities** - Provides utility functions for convenience (like sending replies or proactive messages) **Auth** - Handles authenticating your agent with Teams, Graph, etc. - Simplifies the process of authenticating your app or user for your app **Plugins (Secondary)** - Can hook into activity handlers or proactive scenarios - Can modify or update agent activity events ## Plugins You'll notice that plugins are present in the front, which exposes your application as a server, and also in the back after the app does some processing to the incoming message. The plugin architecture allows the application to be built in an extremely modular way. Each plugin can be swapped out to change or augment the functionality of the application. The plugins can listen to various events that happen (e.g. the server starting or ending, an error occuring, etc), activities being sent to or from the application and more. This allows the application to be extremely flexible and extensible. ================================================ FILE: teams.md/src/pages/templates/essentials/graph.mdx ================================================ --- sidebar_position: 8 sidebar_label: Graph API Client title: Graph API Client summary: Guide to using the Microsoft Graph API client to access Microsoft 365 data and services from your Teams SDK application. --- # Graph API Client [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) gives you access to the wider Microsoft 365 ecosystem. You can enrich your application with data from across Microsoft 365. The SDK gives your application easy access to the Microsoft Graph API via the . ## Calling APIs Microsoft Graph can be accessed by your application using its own application token, or by using the user's token. If you need access to resources that your application may not have, but your user does, you will need to use the user's scoped graph client. To grant explicit consent for your application to access resources on behalf of a user, follow the [auth guide](../in-depth-guides/user-authentication). To access the graph using the Graph using the app, you may use the object . Here, the object is a scoped graph client for the user that sent the message. :::tip You also have access to the object in the activity handler. This is equivalent to . ::: ================================================ FILE: teams.md/src/pages/templates/essentials/on-activity/README.mdx ================================================ --- sidebar_position: 1 summary: Guide to handling Teams-specific activities like chat messages, card actions, and installs using the fluent router API. title: Listening to Activities --- # Listening To Activities An **Activity** is the Teams‑specific payload that flows between the user and your bot. Where _events_ describe high‑level happenings inside your app, _activities_ are the raw Teams messages such as chat text, card actions, installs, or invoke calls. ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting ActivityRouter["Activity Router (app.on())"]:::interesting Handlers["Your Activity Handlers"]:::interesting Teams --> |Events| Server Server --> |Activity Event| ActivityRouter ActivityRouter --> |handler invoked| Handlers classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; classDef less-interesting fill:#666,stroke:#333,stroke-width:4px; ``` Here is an example of a basic message handler: ## Middleware pattern :::info Just like other middlewares, if you stop the chain by not calling `next()`, the activity will not be passed to the next handler. The order of registration for the handlers also matters as that determines how the handlers will be called. ::: ================================================ FILE: teams.md/src/pages/templates/essentials/on-activity/_category_.json ================================================ { "label": "Listening To Activities", "position": 3, "description": "Guide to handling Teams-specific activities like chat messages, card actions, and installs using the fluent router API.", "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/essentials/on-activity/activity-ref.mdx ================================================ --- sidebar_position: 2 sidebar_label: Activity Type Reference title: Activity Type Reference summary: Complete reference guide for all activity types and routes available in Teams SDK applications, including core activities and configuration routes. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # Activity Type Reference The application supports a number of activity types: ## Core Activity Routes | Route | Responsibility | | ---------------- | ----------------------------------------------------------------------------------------------- | | `message` | User messages the app | | `typing` | Sends a typing indicator to indicate the app got the user's message and is computing a response | | `deleteUserData` | Triggered when a user requests their data to be deleted according to privacy regulations | | `mention` | Triggered when the bot is @mentioned in a conversation | ## Configuration Routes | Route | Invoke Path | Responsibility | | --------------- | --------------- | ------------------------------------------------------------- | | `config.open` | `config/fetch` | When app is installed, the user may configure it via a dialog | | `config.submit` | `config/submit` | Configuration dialog submission | | `tab.open` | `tab/fetch` | Initializes tab configuration experiences | | `tab.submit` | `tab/submit` | Processes tab configuration submissions | ## Dialog Routes | Route | Invoke Path | Responsibility | | --------------- | ------------- | ---------------------------- | | `dialog.open` | `task/fetch` | Opens a dialog | | `dialog.submit` | `task/submit` | Processes dialog submissions | ## Authentication Routes | Route | Invoke Path | Responsibility | | ----------------------- | ---------------------- | --------------------------------------------- | | `signin.token-exchange` | `signin/tokenExchange` | When a token exchange happens during SSO Auth | | `signin.verify-state` | `signin/verifyState` | When a verification passes after OAuth | ## Message Interaction Routes | Route | Invoke Path | Responsibility | | ----------------- | --------------------------------- | ----------------------------------------------------------------------- | | `message.execute` | `actionableMessage/executeAction` | An action was executed on a message | | `message.submit` | `message/submitAction` | Handles message action submissions | | `card.action` | `adaptiveCard/action` | Triggered when a user interacts with an Adaptive Card button or control | ## File Handling Routes | Route | Responsibility | | ---------------------- | ------------------------------------------------------------ | | `file.consent` | Manages file sharing permission workflows in Teams | | `file.consent.accept` | Triggered when user accepts a file consent card for sharing | | `file.consent.decline` | Triggered when user declines a file consent card for sharing | ## Message Extension Routes | Route | Invoke Path | Responsibility | | --------------------------------- | -------------------------------------- | ----------------------------------------------------- | | `message.ext.query-link` | `composeExtension/queryLink` | A link unfurling request for an installed application | | `message.ext.anon-query-link` | `composeExtension/anonymousQueryLink` | An anonymous link unfurling request | | `message.ext.query` | `composeExtension/query` | Message extension search query | | `message.ext.select-item` | `composeExtension/selectItem` | Message extension item selection | | `message.ext.submit` | `composeExtension/submitAction` | Message extension action submission | | `message.ext.open` | `composeExtension/fetchTask` | Message extension task fetching for an action | | `message.ext.query-settings-url` | `composeExtension/querySettingUrl` | Retrieves configuration URLs for message extensions | | `message.ext.setting` | `composeExtension/setting` | Processes message extension settings changes | | `message.ext.card-button-clicked` | `composeExtension/onCardButtonClicked` | Card button click handling in message extensions | | `message.ext.edit` | N/A | Processes edits to message extension previews | | `message.ext.send` | N/A | Handles sending of message extension content | ## Lifecycle Routes | Route | Responsibility | | ---------------- | ----------------------------------------------------------- | | `install.add` | Triggered when the app is newly installed to a team or chat | | `install.remove` | Triggered when the app is uninstalled from a team or chat | | `install.update` | Triggered when the app is updated in a team or chat | | `handoff.action` | Manages handoffs from a different agent to your application | ## Conversation Update Routes | Route | Responsibility | | ----------------- | ------------------------------------------------------------------------------------ | | `membersAdded` | Triggered when new users join a team or are added to a chat where the bot is active | | `membersRemoved` | Triggered when users leave a team or are removed from a chat where the bot is active | | `channelCreated` | Triggered when a new channel is created in a team where the bot is installed | | `channelRenamed` | Triggered when a channel is renamed in a team where the bot is installed | | `channelDeleted` | Triggered when a channel is deleted from a team where the bot is installed | | `channelRestored` | Triggered when a previously deleted channel is restored | | `teamArchived` | Triggered when a team is archived | | `teamDeleted` | Triggered when a team is deleted where the bot is installed | | `teamHardDeleted` | Triggered when a team is permanently deleted (beyond recovery) | | `teamRenamed` | Triggered when a team is renamed where the bot is installed | | `teamRestored` | Triggered when a previously deleted team is restored | | `teamUnarchived` | Triggered when a team is unarchived | | `messageUpdate` | Triggered when a message is edited in a conversation with the bot | | `messageDelete` | Triggered when a message is deleted in a conversation with the bot | ## Meeting Routes | Route | Invoke Path | Responsibility | | ------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------- | | `meetingStart` | `application/vnd.microsoft.meetingStart` | Triggered at the beginning of a Teams meeting where the bot is present | | `meetingEnd` | `application/vnd.microsoft.meetingEnd` | Triggered at the end of a Teams meeting where the bot is present | | `meetingParticipantJoin` | `application/vnd.microsoft.meetingParticipantJoin` | Triggered when participants join a Teams meeting where the bot is present | | `meetingParticipantLeave` | `application/vnd.microsoft.meetingParticipantLeave` | Triggered when participants leave a Teams meeting where the bot is present | | `readReceipt` | `application/vnd.microsoft.readReceipt` | Tracks when messages are read by users | ================================================ FILE: teams.md/src/pages/templates/essentials/on-event.mdx ================================================ --- sidebar_position: 2 sidebar_label: Listening to Events title: Listening to Events summary: Understanding how to listen to and handle events in Teams SDK applications, including user actions and application server events. --- # Listening To Events An **event** is a foundational concept in building agents — it represents something noteworthy happening either on Microsoft Teams or within your application. These events can originate from the user (e.g. installing or uninstalling your app, sending a message, submitting a form), or from your application server (e.g. startup, error in a handler). The Teams SDK makes it easy to subscribe to these events and respond appropriately. You can register event handlers to take custom actions when specific events occur — such as logging errors, triggering workflows, or sending follow-up messages. Here are the events that you can start building handlers for: ### Example 1 We can subscribe to errors that occur in the app. ### Example 2 ================================================ FILE: teams.md/src/pages/templates/essentials/sending-messages/README.mdx ================================================ --- sidebar_position: 4 summary: Guide to sending messages from your Teams SDK agent, including replies, proactive messages, and different message types. title: Sending Messages --- # Sending Messages Sending messages is a core part of an agent's functionality. With all activity handlers, a `send` method is provided which allows your handlers to send a message back to the user to the relevant conversation. In the above example, the handler gets a `message` activity, and uses the `send` method to send a reply to the user. You are not restricted to only replying to `message` activities. In the above example, the handler is listening to events, which are sent when a user successfully signs in. :::tip This shows an example of sending a text message. Additionally, you are able to send back things like [adaptive cards](../../in-depth-guides/adaptive-cards) by using the same `send` method. Look at the [adaptive card](../../in-depth-guides/adaptive-cards) section for more details. ::: ## Streaming You may also stream messages to the user which can be useful for long messages, or AI generated messages. The SDK makes this simple for you by providing a `stream` function which you can use to send messages in chunks. :::note Streaming is currently only supported in 1:1 conversations, not group chats or channels ::: ![Animated image showing agent response text incrementally appearing in the chat window.](/screenshots/streaming-chat.gif) ## @Mention Sending a message at `@mentions` a user is as simple including the details of the user using the method ## Targeted Messages :::info[Preview] Targeted messages are currently in preview. ::: Targeted messages, also known as ephemeral messages, are delivered to a specific user in a shared conversation. From a single user's perspective, they appear as regular inline messages in a conversation. Other participants won't see these messages, making them useful for authentication flows, help or error responses, personal reminders, or sharing contextual information without cluttering the group conversation. To send a targeted message when responding to an incoming activity, use the method with the recipient account and set the targeting flag to true. ### Targeted messages in preview ## Reactions :::info[Preview] Reactions are currently in preview. ::: Reactions allow your agent to add or remove emoji reactions on messages in a conversation. The reactions client is available via the API client. ### Reactions in preview ================================================ FILE: teams.md/src/pages/templates/essentials/sending-messages/_category_.json ================================================ { "label": "Sending Messages", "position": 5, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/essentials/sending-messages/proactive-messaging.mdx ================================================ --- sidebar_position: 1 sidebar_label: 'Proactive Messaging' title: 'Proactive Messaging' summary: Learn how to send proactive messages to users without waiting for them to initiate the conversation, including storing conversation IDs and sending notifications. --- # Proactive Messaging In [Sending Messages](./), you were shown how to respond to an event when it happens. However, there are times when you want to send a message to the user without them sending a message first. This is called proactive messaging. You can do this by using the `send` method in the `app` instance. This approach is useful for sending notifications or reminders to the user. The main thing to note is that you need to have the of the chat or channel that you want to send the message to. It's a good idea to store this value somewhere from an activity handler so that you can use it for proactive messaging later. Then, when you want to send a proactive message, you can retrieve the from storage and use it to send the message. :::tip In this example, you see how to get the using one of the activity handlers. This is a good place to store the conversation id, but you can also do this in other places like when the user installs the app or when they sign in. The important thing is that you have the conversation id stored somewhere so you can use it later. ::: ## Targeted Proactive Messages :::info[Preview] Targeted messages are currently in preview. ::: Targeted messages, also known as ephemeral messages, are delivered to a specific user in a shared conversation. From a single user's perspective, they appear as regular inline messages in a conversation. Other participants won't see these messages. When sending targeted messages proactively, you must explicitly specify the recipient account. ================================================ FILE: teams.md/src/pages/templates/getting-started/README.mdx ================================================ --- title: 🚀 Getting Started sidebar_position: 1 summary: Getting started guide for Teams SDK covering application setup, structure, and local development. llms: ignore-file --- # 🚀 Getting Started This guide will help you set up your first Teams SDK application in . You'll learn the basics of creating an application, understanding its structure, and running it locally. By the end of this guide, you'll have a solid foundation to build upon as you explore more advanced features and capabilities of the SDK. ================================================ FILE: teams.md/src/pages/templates/getting-started/_LLMs.mdx ================================================ --- title: LLMs.txt summary: Links to LLM context files that provide coding assistants with documentation for the Teams SDK. llms: ignore --- # LLMs.txt A common practice to speed up development is using Coding Assistants. To better facilitate this usage, you can provide your coding assistant sufficient context about this SDK by linking your assistant to the SDK's llms.txt files for : ================================================ FILE: teams.md/src/pages/templates/getting-started/_category_.json ================================================ { "label": "Getting Started", "position": 1, "collapsed": false } ================================================ FILE: teams.md/src/pages/templates/getting-started/code-basics.mdx ================================================ --- sidebar_position: 2 sidebar_label: Code Basics title: Code Basics summary: Understanding the structure and key components of a Teams SDK application including the Application class, dependency injection, and project organization. --- # Code Basics After following the guidance in [the quickstart](quickstart) to create your first Teams application, let's review its structure and key components. This knowledge can help you build more complex applications as you progress. ## Project Structure When you create a new Teams application, it generates a directory with this basic structure: ## Core Components Let's break down the simple application from the [quickstart](quickstart) into its core components. ### The App Class The heart of an application is the `App` class. This class handles all incoming activities and manages the application's lifecycle. It also acts as a way to host your application service. The app configuration includes a variety of options that allow you to customize its behavior, including controlling the underlying server, authentication, and other settings. ### Plugins Plugins are a core part of the Teams SDK. They allow you to hook into various lifecycles of the application. The lifecycles include server events (start, stop, initialize, etc.), and also Teams Activity events . In fact, the [DevTools](/developer-tools/devtools) application you already have running is a plugin too. It allows you to inspect and debug your application in real-time. :::warning DevTools is a plugin that should only be used in development mode. It should not be used in production applications since it offers no authentication and allows your application to be accessed by anyone. **Be sure to remove the DevTools plugin from your production code.** ::: ### Message Handling Teams applications respond to various types of activities. The most basic is handling messages: This code: 1. 2. Sends a typing indicator, which renders as an animated ellipsis (…) in the chat. 3. ### Application Lifecycle Your application starts when you run: This code initializes your application server and, when configured for Teams, also authenticates it to be ready for sending and receiving messages. ## Next Steps Now that you understand the basic structure of your Teams application, you're ready to [run it in Teams](running-in-teams). You will learn about Microsoft 365 Agents Toolkit and other important tools that help you with deployment and testing your application. After that, you can: - Add more activity handlers for different types of interactions. See [Listening to Activities](../essentials/on-activity) for more details. - Integrate with external services using the [API Client](../essentials/api). - Add interactive [cards](../in-depth-guides/adaptive-cards) and [dialogs](../in-depth-guides/dialogs). - Implement [AI](../in-depth-guides/ai). Continue on to the next page to learn about these advanced features. ## Other Resources - [Essentials](../essentials) - [Teams concepts](/teams) - [Teams developer tools](/developer-tools) ================================================ FILE: teams.md/src/pages/templates/getting-started/quickstart.mdx ================================================ --- sidebar_position: 1 sidebar_label: Quickstart title: Quickstart summary: Quick start guide for Teams SDK using the Teams CLI to create and run your first agent. --- # Quickstart Get started with Teams SDK quickly using the Teams CLI. ## Set up a new project ### Prerequisites ## Instructions ### Use the Teams CLI Use your terminal to run the Teams CLI using npx: ```sh npx @microsoft/teams.cli --version ``` :::info _The [Teams CLI](/developer-tools/cli) is a command-line tool that helps you create and manage Teams applications. It provides a set of commands to simplify the development process._

Using `npx` allows you to run the Teams CLI without installing it globally. You can verify it works by running the version command above. ::: ## Creating Your First Agent Let's begin by creating a simple echo agent that responds to messages. Run: This command: > The `echo` template creates a basic agent that repeats back any message it receives - perfect for learning the fundamentals. ## Running your agent When the application starts, you'll see: 1. An HTTP server starting up (on port `3978`). This is the main server which handles incoming requests and serves the agent application. 2. A devtools server starting up (on port `3979`). This is a developer server that provides a web interface for debugging and testing your agent quickly, without having to deploy it to Teams. :::info The DevTools server runs on a separate port to avoid conflicts with your main application server. This allows you to test your agent locally while keeping the main server available for Teams integration. ::: Now, navigate to the devtools server by opening your browser and navigating to [http://localhost:3979/devtools](http://localhost:3979/devtools). You should see a simple interface where you can interact with your agent. Try sending it a message! ![Screenshot of DevTools showing user prompt 'hello!' and agent response 'you said hello!'.](/screenshots/devtools-echo-chat.png) ## Add to an Existing Project If you already have a project and want to add Teams support, install the SDK directly: Then initialize the Teams app with your existing server: `app.initialize()` registers the Teams endpoint on your server without starting a new one — you keep full control of your server lifecycle. ## Next steps After creating and running your first agent, read about [the code basics](code-basics) to better understand its components and structure. Otherwise, if you want to run your agent in Teams, you can check out the [Running in Teams](running-in-teams) guide. ## Resources - [Teams CLI documentation](/developer-tools/cli) - [Teams DevTools documentation](/developer-tools/devtools) - [Teams manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) - [Teams sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) ================================================ FILE: teams.md/src/pages/templates/getting-started/running-in-teams/README.mdx ================================================ --- sidebar_position: 3 title: 'Running in Teams' summary: Guide to deploying and testing your locally running agent in Microsoft Teams using the Microsoft 365 Agents Toolkit. llms: ignore --- # Running In Teams Now that you completed [the quickstart](../quickstart) and your agent is running locally, let's deploy it to Microsoft Teams for testing. This guide will walk you through the process. ## Microsoft 365 Agents Toolkit Microsoft 365 Agents Toolkit is a powerful tool that simplifies deploying and debugging Teams applications. It automates tasks like managing the Teams app manifest, configuring authentication, provisioning, and deployment. If you'd like to learn about these concepts, check out [Teams core concepts](/teams/core-concepts). ### Install Microsoft 365 Agents Toolkit First, you'll need to install the Agents Toolkit IDE extension: - Visit the [Microsoft 365 Agents Toolkit installation guide](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/install-teams-toolkit) to install on your preferred IDE. ## Adding Teams configuration files via `teams` CLI To configure your agent for Teams, run the following command in the terminal inside your quote-agent folder: :::tip (if you have `teams` CLI installed globally, use `teams` instead of `npx`) ::: ```bash npx @microsoft/teams.cli config add atk.basic ``` :::tip The `atk.basic` configuration is a basic setup for Agents Toolkit. It includes the necessary files and configuration to get started with Teams development.
Explore more advanced configurations as needed with `npx @microsoft/teams.cli config --help`.
::: This [CLI](/developer-tools/cli) command adds configuration files required by Agents Toolkit, including: - Environment setup in the `env` folder and root `.env` file - Teams app manifest in the `appPackage` folder (if not already present) - Debug instructions in `.vscode/launch.json` and `.vscode/tasks.json` - Agents Toolkit automation files to your project (e.g. `teamsapp.local.yml`) | Tool Name | Command | Description | | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Teams SDK CLI | `teams` | A command-line tool for setting up and utilizing the Teams SDK, including integration with Microsoft 365 Agents Toolkit when desired. | | Microsoft 365 Agents Toolkit | `atk` | A tool for managing provisioning, deployment, and in-client debugging for Teams applications. | ## Debugging in Teams After installing Agents Toolkit and adding the configuration: 1. **Open** your agent's project in your IDE. 2. **Open the Microsoft 365 Agents Toolkit extension panel** (usually on the left sidebar). 3. **Log in** to your Microsoft 365 and Azure accounts in the Agents Toolkit extension. 4. **Select "Local"** under Environment Settings of the Agents Toolkit extension. 5. **Click on Debug (Chrome) or Debug (Edge)** to start debugging via the 'play' button. ![Screenshot of Microsoft 365 Agents Toolkit with 'Environment' section expanded and 'local' selected.](/screenshots/agents-toolkit.png) When debugging starts, the Agents Toolkit will: - **Build** your application - **Start a [devtunnel](/teams/core-concepts#devtunnel)** that will assign a temporary public URL to your local server - **Provision the Teams app** for your tenant so that it can be installed and be authenticated on Teams - **Set up the local variables** necessary for your agent to run in Teams in `env/.env.local` and `env/env.local.user`. This includes propagating the app manifest with your newly provisioned resources. - **Start** the local server. - **Package your app manifest** into a Teams application zip package and the manifest json with variables inserted in `appPackage/build`. - **Launch Teams** in an incognito window in your browser. - **Upload the package** to Teams and signal it to sideload (install) the app just for your use. If you set up Agents Toolkit via the Teams SDK CLI, you should see something like the following in your terminal: ## Testing your agent After the debugging session starts: 1. Teams will open in your browser 2. You'll be prompted to sign in (if not already) 3. Teams will ask permission to install the app 4. Once installed, you can start chatting with your agent! ![Screenshot of `quote-agent-local` agent running in Teams.](/screenshots/example-on-teams.png) Congratulations! Now you have a fully functional agent running in Microsoft Teams. Interact with it just like any other Teams app and explore the rest of the documentation to build more complex agents. :::tip If you want to monitor the activities and events in your app, you can still use the [DevTools plugin](/developer-tools/devtools)! Note that the DevTools server is running on port 3979. You can open it in your browser to interact with your agent and monitor activities in real time. ::: ## Troubleshooting For deployment and resource management we recommend the Microsoft 365 Agents Toolkit. For authentication-related issues, refer to our [Authentication Troubleshooting](/teams/app-authentication/troubleshooting) guide. If you prefer to set everything up by hand, follow our [Manual Configuration](/teams/configuration/manual-configuration) guide. The Teams SDK itself doesn't handle deployment or Azure resources, so you'll need to rely on the general [Microsoft Teams deployment documentation](https://learn.microsoft.com/en-us/microsoftteams/deploy-overview) for in-depth help. ## Next steps Now that your agent is running in Teams, you can learn more [essential concepts](../../essentials) to understand how to build more complex agents. Explore the [in-depth guides](../../in-depth-guides) for advanced topics like authentication, message extensions, and more. ## Resources - [Teams CLI documentation](/developer-tools/cli) - [Microsoft 365 Agents Toolkit documentation](https://learn.microsoft.com/en-us/microsoft-365/developer/overview-m365-agents-toolkit?toc=%2Fmicrosoftteams%2Fplatform%2Ftoc.json&bc=%2Fmicrosoftteams%2Fplatform%2Fbreadcrumb%2Ftoc.json) - [Microsoft 365 Agents Toolkit CLI documentation](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli) - [Teams CLI GitHub repository](https://github.com/OfficeDev/Teams-Toolkit) - [Microsoft Teams deployment documentation](https://learn.microsoft.com/en-us/microsoftteams/deploy-overview) ================================================ FILE: teams.md/src/pages/templates/getting-started/running-in-teams/_category_.json ================================================ { "position": 3, "label": "Running in Teams", "collapsible": true, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/README.mdx ================================================ --- title: In-Depth Guides sidebar_position: 3 summary: Advanced guides covering complex topics like AI integration, adaptive cards, dialogs, message extensions, and user authentication. --- # In-Depth Guides This section provides comprehensive technical guides for integration with useful Teams features. Learn how to implement AI-powered bots, create adaptive cards, manage authentication flows, and build sophisticated message extensions. Each guide includes practical examples and best practices for production applications. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/_category_.json ================================================ { "label": "In-Depth Guides", "position": 3, "collapsible": true, "collapsed": false } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/adaptive-cards/README.mdx ================================================ --- title: Adaptive Cards sidebar_position: 1 summary: Introduction to Adaptive Cards in Teams SDK applications for creating rich, interactive user experiences across various scenarios. --- # Adaptive Cards Adaptive Cards provide a flexible, cross-platform content format for creating rich, interactive experiences. They consist of a customizable body of card elements combined with optional action sets, all fully serializable for delivery to clients. Through a powerful combination of text, graphics, and interactive buttons, Adaptive Cards enable compelling user experiences across various platforms. The Adaptive Card framework is widely implemented throughout Microsoft's ecosystem, with significant integration in Microsoft Teams. Within Teams, Adaptive Cards power numerous key scenarios including: - Rich interactive messages - Dialogs - Message Extensions - Link Unfurling - Configuration forms - And many more application contexts Mastering Adaptive Cards is essential for creating sophisticated, engaging experiences that leverage the full capabilities of the Teams platform. This guide will help you learn how to use them in this SDK. For a more comprehensive guide on Adaptive Cards, see the [official documentation](https://adaptivecards.microsoft.com/). ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/adaptive-cards/_category_.json ================================================ { "label": "Adaptive Cards", "position": 1, "collapsible": true, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/adaptive-cards/building-adaptive-cards.mdx ================================================ --- sidebar_position: 1 sidebar_label: Building Adaptive Cards title: Building Adaptive Cards summary: Guide to building Adaptive Cards with builder helpers for type-safe, maintainable UI development. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Building Adaptive Cards Adaptive Cards are JSON payloads that describe rich, interactive UI fragments. ## The Builder Pattern Each helper wraps raw JSON and provides fluent, chainable methods that keep your code concise and readable. Benefits: | Benefit | Description | | ----------- | ----------------------------------------------------------------------------- | | Readability | No deep JSON trees—just chain simple methods. | | Re‑use | Extract snippets to functions or classes and share across cards. | | Safety | Builders validate every property against the Adaptive Card schema (see next). | ## Type‑safe Authoring & IntelliSense The package bundles the **Adaptive Card v1.5 schema** as strict types. While coding you get: - **Autocomplete** for every element and attribute. - **In‑editor validation**—invalid enum values or missing required properties produce build errors. - Automatic upgrades when the schema evolves; simply update the package. ## The Visual Designer Prefer a drag‑and‑drop approach? Use [Microsoft's Adaptive Card Designer](https://adaptivecards.microsoft.com/designer.html): 1. Add elements visually until the card looks right. 2. Copy the JSON payload from the editor pane. 3. Paste the JSON into your project **or** convert it to builder calls: This method leverages the full Adaptive Card schema and ensures that the payload adheres strictly to . :::tip You can use a combination of raw JSON and builder helpers depending on whatever you find easier. ::: ## End‑to‑end Example – Task Form Card Below is a complete example showing a task management form. ## Additional Resources - [**Official Adaptive Card Documentation**](https://adaptivecards.microsoft.com/) - [**Adaptive Cards Designer**](https://adaptivecards.microsoft.com/designer.html) ### Summary - Use **builder helpers** for readable, maintainable card code. - Enjoy **full type safety** and IDE assistance. - Prototype quickly in the **visual designer** and refine with builders. Happy card building! 🎉 ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/adaptive-cards/executing-actions.mdx ================================================ --- sidebar_position: 2 sidebar_label: Executing Actions title: Executing Actions summary: How to implement interactive elements in Adaptive Cards through actions like buttons, links, and input submission triggers. --- # Executing Actions Adaptive Cards support interactive elements through **actions**—buttons, links, and input submission triggers that respond to user interaction. You can use these to collect form input, trigger workflows, show task modules, open URLs, and more. ## Action Types The Teams SDK supports several action types for different interaction patterns: | Action Type | Purpose | Description | | ------------------------- | ---------------------- | ---------------------------------------------------------------------------- | | `Action.Execute` | Server‑side processing | Send data to your bot for processing. Best for forms & multi‑step workflows. | | `Action.Submit` | Simple data submission | Legacy action type. Prefer `Execute` for new projects. | | `Action.OpenUrl` | External navigation | Open a URL in the user's browser. | | `Action.ShowCard` | Progressive disclosure | Display a nested card when clicked. | | `Action.ToggleVisibility` | UI state management | Show/hide card elements dynamically. | :::info For complete reference, see the [official documentation](https://adaptivecards.microsoft.com/?topic=Action.Execute). ::: ## Creating Actions with the SDK ### Single Actions The SDK provides builder helpers that abstract the underlying JSON. For example: ### Action Sets Group actions together using `ActionSet`: ### Raw JSON Alternative Just like when building cards, if you prefer to work with raw JSON, you can do just that. ## Working with Input Values ### Associating data with the cards Sometimes you want to send a card and have it be associated with some data. Set the `data` value to be sent back to the client so you can associate it with a particular entity. ### Input Validation Input Controls provide ways for you to validate. More details can be found on the Adaptive Cards [documentation](https://adaptivecards.microsoft.com/?topic=input-validation). ## Server Handlers ### Basic Structure Card actions arrive as `card.action` activities in your app. These give you access to the validated input values plus any `data` values you had configured to be sent back to you. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/README.mdx ================================================ --- title: 🤖 AI sidebar_position: 5 summary: Overview of AI components in Teams SDK, including Prompts for orchestration and Models for LLM interfaces. --- # 🤖 AI The AI packages in this SDK are designed to make it easier to build applications with LLMs. The has two main components: ## 📦 Prompts A `Prompt` is the component that orchestrates everything, it handles state management, function definitions, and invokes the model/template when needed. This layer abstracts many of the complexities of the Models to provide a common interface. ## 🧠 Models A `Model` is the component that interfaces with the LLM, being given some `input` and returning the `output`. This layer deals with any of the nuances of the particular Models being used. It is in the model implementation that the individual LLM features (i.e. streaming/tools etc.) are made compatible with the more general features of the . :::note You are not restricted to use the to build your Teams Agent applications. You can use models directly if you choose. These packages are there to simplify the interactions with the models and Teams. ::: ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/_category_.json ================================================ { "label": "🤖 AI", "key": "ai-guide", "position": 5, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/a2a/README.mdx ================================================ --- summary: Overview of the experimental A2A (Agent-to-Agent) protocol for enabling programmatic communication between AI agents. languages: ['typescript', 'python'] title: A2A Protocol --- # A2A (Agent-to-Agent) Protocol [What is A2A?](https://a2a-protocol.org/latest/) A2A (Agent-to-Agent) is a protocol designed to enable agents to communicate and collaborate programmatically. This package allows you to integrate the A2A protocol into your Teams app, making your agent accessible to other A2A clients and enabling your app to interact with other A2A servers. ## What does this package do? - **A2A Server**: Enables your Teams agent to act as an A2A server, exposing its capabilities to other agents through the `/a2a` endpoint and serving an agent card at `/a2a/.well-known/agent-card.json`. - **A2A Client**: Allows your Teams app to proactively reach out to other A2A servers as a client, either through direct `AgentManager` usage or integrated with `ChatPrompt` for LLM-driven interactions. ## High-level Architecture ### A2A Server ```mermaid flowchart RL A_S[TeamsApp] B[A2APlugin] D[External A2A Client] D -- "task/send" message --> A_S subgraph A2A Server direction LR A_S --> B end B -- AgentCard --> D B -- "task/send" response --> D ``` ### A2A Client ```mermaid flowchart LR A_C[TeamsApp] C[A2AClientPlugin] E[External A2A Server] U[Teams User] U --> A_C subgraph A2A Client direction LR A_C -- message --> C C -- response from server --> A_C end C -- message task/send --> E E -- AgentCard --> C E -- task/send response --> C ``` ## Protocol Details For detailed information about the A2A protocol, including agent card structure, message formats, and protocol specifications, see the official [A2A Protocol Documentation](https://a2a-protocol.org/latest/specification/). ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/a2a/_category_.json ================================================ { "label": "A2A (Agent-to-Agent)", "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/a2a/a2a-client.mdx ================================================ --- sidebar_position: 2 title: A2A Client sidebar_label: A2A Client summary: How to implement an A2A client to proactively send tasks to A2A servers using the AgentManager. languages: ['typescript', 'python'] --- # A2A Client ## What is an A2A Client? An A2A client is an agent or application that can proactively send tasks to A2A servers and interact with them using the A2A protocol. ## Using `A2AClient` Directly For direct control over A2A interactions, you can use the `A2AClient` from the SDK: ## Using `A2AClientPlugin` with ChatPrompt A2A is most effective when used with an LLM. The `A2AClientPlugin` can be added to your chat prompt to allow interaction with A2A agents. Once added, the plugin will automatically configure the system prompt and tool calls to determine if the a2a server is needed for a particular task, and if so, it will do the work of orchestrating the call to the A2A server. To send a message: ### Advanced `A2AClientPlugin` Configuration You can customize how the client interacts with A2A agents by providing custom builders: ## Sequence Diagram Here's how the A2A client works with `ChatPrompt` and `A2AClientPlugin`: ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/a2a/a2a-server.mdx ================================================ --- sidebar_position: 1 title: A2A Server summary: How to implement an A2A server to expose your Teams app capabilities to other agents using the A2A protocol. languages: ['typescript', 'python'] --- import FileCodeBlock from '@site/src/components/FileCodeBlock'; # A2A Server ## What is an A2A Server? An A2A server is an agent that exposes its capabilities to other agents using the A2A protocol. With this package, you can make your Teams app accessible to A2A clients. ## Adding the `A2APlugin` To enable A2A server functionality, add the `A2APlugin` to your Teams app and provide an : ## Agent Card Exposure The plugin automatically exposes your agent card at the path `/a2a/.well-known/agent-card.json`. ## Handling A2A Requests Handle incoming A2A requests by adding an event handler for the `a2a:message` event. You may use `accumulateArtifacts` to iteratively accumulate artifacts for the task, or simply `respond` with the final result. :::note - You must have only a single handler that calls `respond`. - You **must** call `respond` as the last step in your handler. This resolves the open request to the caller. ::: ## Sequence Diagram ```mermaid sequenceDiagram participant A2A Client participant App participant A2APlugin participant YourEventHandler A2A Client->>App: /task/send App->>A2APlugin: Call A2APlugin A2APlugin->>YourEventHandler: Call your event handler a2a:message YourEventHandler->>A2APlugin: Call respond A2APlugin->>A2A Client: Return response ``` ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/best-practices.mdx ================================================ --- sidebar_position: 5 sidebar_label: Best Practices title: Best Practices summary: Best practices for AI integration in Teams applications, including AI-generated message indicators, feedback collection for prompt improvement, and citation handling to ensure transparency and accuracy in AI responses. --- # Best Practices When sending messages using AI, Teams recommends a number of best practices to help with both user and developer experience. ## AI-Generated Indicator When sending messages using AI, Teams recommends including an indicator that the message was generated by AI. This will help users understand that the message was generated by AI, and not by a human and can help with trust and transparency. ![Screenshot of outgoing agent message to user marked with 'AI generated' badge.](/screenshots/ai-generated.gif) ## Gather feedback to improve prompts AI Generated messages are not always perfect. Prompts can have gaps, and can sometimes lead to unexpected results. To help improve the prompts, Teams recommends gathering feedback from users on the AI-generated messages. See [Feedback](../feedback) for more information on how to gather feedback. This does involve thinking through a pipeline for gathering feedback and then automatically, or manually, updating prompts based on the feedback. The feedback system is an point of entry to your eval pipeline. ## Citations AI generated messages can hallucinate even if messages are grounded in real data. To help with this, Teams recommends including citations in the AI Generated messages. :::warning Citations are added with a `position` property. This property value needs to also be included in the message text as `[]`. If there is a citation that's added without the associated value in the message text, Teams will not render the citation ::: ![Animated screenshot showing user hovering over a footnote citation in agent response, and a pop-up showing explanatory text.](/screenshots/citation.gif) ## Suggested actions Suggested actions help users with ideas of what to ask next, based on the previous response or conversation. Teams recommends including suggested actions in your messages. See [Suggested actions](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/conversations/prompt-suggestions) for more information on suggested actions. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/chat.mdx ================================================ --- sidebar_position: 2 sidebar_label: 💬 Chat Generation title: 💬 Chat Generation summary: Comprehensive guide to implementing chat generation with LLMs in Teams, covering setup with ChatPrompt and Model objects, basic message handling, and streaming responses for improved user experience. --- # 💬 Chat Generation Before going through this guide, please make sure you have completed the [setup and prerequisites](./setup-and-prereqs.mdx) guide. # Setup The basic setup involves creating a `ChatPrompt` and giving it the `Model` you want to use. ```mermaid flowchart LR Prompt subgraph Application Send --> Prompt UserMessage["User Message
Hi how are you?"] --> Send Send --> Content["Content
I am doing great! How can I help you?"] subgraph Setup Messages --> Prompt Instructions --> Prompt Options["Other options..."] --> Prompt Prompt --> Model end end subgraph LLMProvider Model --> AOAI["Azure Open AI"] Model --> OAI["Open AI"] Model --> Anthropic["Claude"] Model --> OtherModels["..."] end ``` ## Simple chat generation Chat generation is the the most basic way of interacting with an LLM model. It involves setting up your ChatPrompt, the Model, and sending it the message. ## Streaming chat responses LLMs can take a while to generate a response, so often streaming the response leads to a better, more responsive user experience. :::warning Streaming is only currently supported for single 1:1 chats, and not for groups or channels. ::: ![Animated image showing agent response text incrementally appearing in the chat window.](/screenshots/streaming-chat.gif) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/function-calling.mdx ================================================ --- sidebar_position: 3 sidebar_label: Function Calling title: Function Calling summary: How to implement function calling in AI models, allowing the LLM to execute functions as part of its response generation. --- # Function / Tool calling It's possible to hook up functions that the LLM can decide to call if it thinks it can help with the task at hand. This is done by . ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/keeping-state.mdx ================================================ --- sidebar_position: 4 sidebar_label: Keeping State title: Keeping State summary: Guide to managing conversation state in LLM interactions, explaining how to maintain chat history using ChatPrompt's state management capabilities and implementing custom persistence strategies for multi-conversation scenarios. --- # Keeping State By default, LLMs are not stateful. This means that they do not remember previous messages or context when generating a response. It's common practice to keep state of the conversation history in your application and pass it to the LLM each time you make a request. By default, the `ChatPrompt` instance will create a temporary in-memory store to keep track of the conversation history. This is beneficial when you want to use it to generate an LLM response, but not persist the conversation history. But in other cases, you may want to keep the conversation history :::warning By reusing the same `ChatPrompt` class instance across multiple conversations will lead to the conversation history being shared across all conversations. Which is usually not the desired behavior. ::: To avoid this, you need to get messages from your persistent (or in-memory) store and pass it in to the `ChatPrompt`. :::note The `ChatPrompt` class will modify the messages object that's passed into it. So if you want to manually manage it, you need to make a copy of the messages object before passing it in. ::: ## State Initialization Here's how to initialize and manage conversation state for multiple conversations: ## Usage Example ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/mcp/README.mdx ================================================ --- sidebar_position: 6 title: MCP summary: Overview of Model Context Protocol (MCP) integration in Teams SDK for dynamic function and tool loading. suppressLanguageIncludeWarning: true --- # MCP Teams SDK has optional packages which support the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) as a service or client. This allows you to use MCP to call functions and tools in your application. MCP servers and MCP clients dynamically load function definitions and tools. When building Servers, this could mean that you can introduce new tools as part of your application, and the MCP clients that are connected to it will automatically start consuming those tools. When building Clients, this could mean that you can connect to other MCP servers and your application has the flexibility to improve as the MCP servers its connected to evolve over time. :::tip The guides here can be used to build a server and a client that can leverage each other. That means you can build a server that has the ability to do complex things for the client agent. ::: ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/mcp/_category_.json ================================================ { "label": "MCP", "key": "mcp-guide", "position": 6, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/mcp/mcp-client.mdx ================================================ --- sidebar_position: 2 sidebar_label: MCP Client title: MCP Client summary: How to implement an MCP client to leverage remote MCP servers and their tools in your AI agent application. --- # MCP Client You are able to leverage other MCP servers that expose tools via the as part of your application. This allows your AI agent to use remote tools to accomplish tasks. :::info Take a look at [Function calling](../function-calling) to understand how the `ChatPrompt` leverages tools to enhance the LLM's capabilities. MCP extends this functionality by allowing remote tools, that may or may not be developed or maintained by you, to be used by your application. ::: ## Remote MCP Server The first thing that's needed is access to a **remote** MCP server. MCP Servers (at present) come using two main types protocols: 1. StandardIO - This is a _local_ MCP server, which runs on your machine. An MCP client may connect to this server, and use standard input and outputs to communicate with it. Since our application is running remotely, this is not something that we want to use 2. - This is a _remote_ MCP server. An MCP client may send it requests and the server responds in the expected MCP protocol. For hooking up to your server, you will need to know the URL of the server, and if applicable, that must be included as part of the header. ## MCP Client Plugin The integrates directly with the `ChatPrompt` . When the `ChatPrompt`'s function is called, it calls the external MCP server and loads up all the tools that are available to it. Once loaded, it treats these tools like any functions that are available to the `ChatPrompt` object. If the LLM then decides to call one of these remote MCP tools, the MCP Client plugin will call the remote MCP server and return the result back to the LLM. The LLM can then use this result in its response. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/mcp/mcp-server.mdx ================================================ --- sidebar_position: 1 sidebar_label: MCP Server title: MCP Server summary: How to convert your Teams app into an MCP server using the McpPlugin to expose tools, resources, and prompts to other MCP applications. languages: ['typescript', 'python'] --- # MCP Server ## Piping messages to the user Since your agent is provisioned to work on Teams, one very helpful feature is to use this server as a way to send messages to the user. This can be helpful in various scenarios: 1. Human in the loop - if the server or an MCP client needs to confirm something with the user, it is able to do so. 2. Notifications - the server can be used as a way to send notifications to the user. Here is an example of how to do this. Configure your plugin so that: 1. It can validate if the incoming request is allowed to send messages to the user 2. It fetches the correct conversation ID for the given user. 3. It sends a proactive message to the user. See [Proactive Messaging](../../../essentials/sending-messages/proactive-messaging) for more details. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/ai/setup-and-prereqs.mdx ================================================ --- sidebar_position: 1 sidebar_label: Setup & Prerequisites title: Setup & Prerequisites summary: Prerequisites and setup guide for integrating LLMs into Teams SDK applications, including API keys and configuration. --- # Setup & Prerequisites There are a few prerequisites to getting started with integrating LLMs into your application: - LLM API Key - To generate messages using an LLM, you will need to have an API Key for the LLM you are using. - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) - [OpenAI](https://platform.openai.com/) - In your application, you should include your keys in a secure way. ### Azure OpenAI You will need to deploy a model in Azure OpenAI. View the [resource creation guide](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model 'Azure OpenAI Model Deployment Guide') for more information on how to do this. ### OpenAI You will need to create an OpenAI account and get an API key. View the [OpenAI Quickstart Guide](https://platform.openai.com/docs/quickstart/build-your-application 'OpenAI Quickstart Guide') for how to do this. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/dialogs/README.mdx ================================================ --- sidebar_position: 2 title: 'Dialogs' suppressLanguageIncludeWarning: true --- # Dialogs Dialogs are a helpful paradigm in Teams which improve interactions between your agent and users. When dialogs are **invoked**, they pop open a window for a user in the Teams client. The content of the dialog can be supplied by the agent application. :::note In Teams client v1, dialogs were called task modules. They may occasionaly be used synonymously. ::: ## Key benefits 1. Dialogs pop open for a user in the Teams client. This means in group-settings, dialog actions are not visible to other users in the channel, reducing clutter. 2. Interactions like filling out complex forms, or multi-step forms where each step depends on the previous step are excellent use cases for dialogs. 3. The content for the dialog can be hard-coded in, or fetched at runtime. This makes them extremely flexible and powerful. ## Resources - [Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules) - [Invoking Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/invoking-task-modules) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/dialogs/_category_.json ================================================ { "position": 2, "label": "Dialogs (Task Modules)", "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/dialogs/creating-dialogs.mdx ================================================ --- sidebar_position: 1 sidebar_label: 'Creating Dialogs' title: 'Creating Dialogs' --- # Creating Dialogs :::tip If you're not familiar with how to build Adaptive Cards, check out [the cards guide](../adaptive-cards). Understanding their basics is a prerequisite for this guide. ::: ## Entry Point ## Handling Dialog Open Events ### Rendering A Card You can render an Adaptive Card in a dialog by returning a card response. :::info The action type for submitting a dialog must be `Action.Submit`. This is a requirement of the Teams client. If you use a different action type, the dialog will not be submitted and the agent will not receive the submission event. ::: ### Rendering A Webpage You can render a webpage in a dialog as well. There are some security requirements to be aware of: 1. The webpage must be hosted on a domain that is allow-listed as `validDomains` in the Teams app [manifest](/teams/manifest) for the agent 2. The webpage must also host the [teams-js client library](https://www.npmjs.com/package/@microsoft/teams-js). The reason for this is that for security purposes, the Teams client will not render arbitrary webpages. As such, the webpage must explicitly opt-in to being rendered in the Teams client. Setting up the teams-js client library handles this for you. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/dialogs/handling-dialog-submissions.mdx ================================================ --- sidebar_position: 2 sidebar_label: Handling Dialog Submissions title: 'Handling Dialog Submissions' summary: Guide to processing dialog submissions in Teams applications, showing how to handle form data from both Adaptive Cards and web pages using dialog submission event handlers. --- # Handling Dialog Submissions In this example, we show how to handle dialog submissions from an Adaptive Card form: Similarly, handling dialog submissions from rendered webpages is also possible: ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/dialogs/handling-multi-step-forms.mdx ================================================ --- sidebar_position: 3 sidebar_label: Handling Multi-Step Forms title: 'Handling Multi-Step Forms' summary: Tutorial on implementing multi-step dialogs in Teams, demonstrating how to create dynamic form flows that adapt based on user input, with examples of handling state between steps and conditional navigation. --- # Handling Multi-Step Forms Dialogs can become complex yet powerful with multi-step forms. These forms can alter the flow of the survey depending on the user's input or customize subsequent steps based on previous answers. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/feedback.mdx ================================================ --- sidebar_position: 7 title: Feedback sidebar_label: Feedback summary: Guide to implementing user feedback functionality in Teams applications, covering feedback UI components, event handling, and storage mechanisms for gathering and managing user responses to improve application performance. --- # Feedback User feedback is essential for the improvement of any application. Teams provides specialized UI components to help facilitate the gathering of feedback from users. ![Animated image showing user selecting the thumbs-up button on an agent response and a dialog opening asking 'What did you like?'. The user types 'Nice' and hits Submit.](/screenshots/feedback.gif) ## Storage Once you receive a feedback event, you can choose to store it in some persistent storage. In the example below, we are storing it in an in-memory store. ## Including Feedback Buttons When sending a message that you want feedback in, simply add feedback functionality to the message you are sending. ## Handling the feedback Once the user decides to like/dislike the message, you can handle the feedback in a received event. Once received, you can choose to include it in your persistent store. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/meeting-events.mdx ================================================ --- sidebar_position: 8 title: Meeting Events sidebar_label: Meeting Events summary: Guide to handling meeting events in Teams applications, covering meeting lifecycle events such as meeting start, meeting end, participant join, and participant leave events. --- # Meeting Events Microsoft Teams provides meeting events that allow your application to respond to various meeting lifecycle changes. Your app can listen to events like when a meeting starts, meeting ends, and participant activities to create rich, interactive experiences. ## Overview Meeting events enable your application to: - Send notifications when meetings start or end - Track participant activity (join/leave events) - Display relevant information or cards based on meeting context - Integrate with meeting workflows ## Configuring Your Bot There are a few requirements in the Teams app manifest (`manifest.json`) to support these events. 1. The scopes section must include `team`, and `groupChat` ```json bots": [ { "botId": "", "scopes": [ "team", "personal", "groupChat" ], "isNotificationOnly": false } ] ``` 2. In the authorization section, make sure to specify the following resource-specific permissions: ```json "authorization":{ "permissions":{ "resourceSpecific":[ { "name":"OnlineMeetingParticipant.Read.Chat", "type":"Application" }, { "name":"ChannelMeeting.ReadBasic.Group", "type":"Application" }, { "name":"OnlineMeeting.ReadBasic.Chat", "type":"Application" } ] } } ``` 3. In the Teams Developer Portal, for your `Bot`, make sure the `Meeting Event Subscriptions` are checked off. This enables you to receive the Meeting Participant events. For these events, you must create your Bot via TDP. ## Meeting Start Event When a meeting starts, your app can handle the `meetingStart` event to send a notification or card to the meeting chat. ## Meeting End Event When a meeting ends, your app can handle the `meetingEnd` event to send a summary or follow-up information. ## Participant Join Event When a participant joins a meeting, your app can handle the `meetingParticipantJoin` event to welcome them or display their role. ## Participant Leave Event When a participant leaves a meeting, your app can handle the `meetingParticipantLeave` event to notify others. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/README.mdx ================================================ --- title: Message Extensions sidebar_position: 3 summary: Overview of message extensions for enhancing user productivity with quick access to information and actions within Teams. suppressLanguageIncludeWarning: true --- # 📖 Message Extensions Message extensions (or Compose Extensions) allow your application to hook into messages that users can send or perform actions on messages that users have already sent. They enhance user productivity by providing quick access to information and actions directly within the Teams interface. Users can search or initiate actions from the compose message area, the command box, or directly from a message, with the results returned as richly formatted cards that make information more accessible and actionable. There are two types of message extensions: [API-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/api-based-overview) and [Bot-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/build-bot-based-message-extension?tabs=search-commands). API-based message extensions use an OpenAPI specification that Teams directly queries, requiring no additional application to build or maintain, but offering less customization. Bot-based message extensions require building an application to handle queries, providing more flexibility and customization options. This SDK supports bot-based message extensions only. ## Resources - [What are message extensions?](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=desktop) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/_category_.json ================================================ { "label": "📖 Message Extensions", "position": 3, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/action-commands.mdx ================================================ --- sidebar_position: 1 sidebar_label: 'Action Commands' title: 'Action Commands' summary: Learn how to create action commands for message extensions that present modal dialogs to collect or display information in Teams. --- # Action commands Action commands allow you to present your users with a modal pop-up called a dialog in Teams. The dialog collects or displays information, processes the interaction, and sends the information back to Teams compose box. ## Action command invocation locations There are three different areas action commands can be invoked from: 1. Compose Area 2. Compose Box 3. Message ### Compose Area and Box ![Screenshot of Teams with outlines around the 'Compose Box' (for typing messages) and the 'Compose Area' (the menu option next to the compose box that provides a search bar for actions and apps).](/screenshots/compose-area.png) ### Message action command ![Screenshot of message extension response in Teams. By selecting the '...' button, a menu has opened with 'More actions' option in which they can select from a list of available message extension actions.](/screenshots/message.png) :::tip See the [Invoke Locations](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command?tabs=Teams-toolkit%2Cdotnet#select-action-command-invoke-locations) guide to learn more about the different entry points for action commands. ::: ## Setting up your Teams app manifest To use action commands you have define them in the Teams app manifest. Here is an example: ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "createCard", "type": "action", "context": [ "compose", "commandBox" ], "description": "Command to run action to create a card from the 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": "getMessageDetails", "type": "action", "context": [ "message" ], "description": "Command to run action on message context.", "title": "Get Message Details" }, { "id": "fetchConversationMembers", "description": "Fetch the conversation members", "title": "Fetch Conversation Members", "type": "action", "fetchTask": true, "context": [ "compose" ] }, ] } ] ``` Here we have defining three different commands: 1. `createCard` - that can be invoked from either the `compose` or `commandBox` areas. Upon invocation a dialog will popup asking the user to fill the `title`, `subTitle`, and `text`. ![Screenshot of a message extension dialog with the editable fields 'Card title', 'Subtitle', and 'Text'.](/screenshots/parameters.png) 2. `getMessageDetails` - It is invoked from the `message` overflow menu. Upon invocation the message payload will be sent to the app which will then return the details like `createdDate`, etc. ![Screenshot of the 'More actions' message extension menu expanded with 'Get Message Details' option selected.](/screenshots/message-command.png) 3. `fetchConversationMembers` - It is invoked from the `compose` area. Upon invocation the app will return an adaptive card in the form of a dialog with the conversation roster. ![Screenshot of the 'Fetch Conversation Members' option exposed from the message extension menu '...' option.](/screenshots/fetch-conversation-members.png) ## Handle submission ### Create card ### Create message details card ## Handle opening adaptive card dialog ### Create conversation members card ## Resources - [Action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command?tabs=Teams-toolkit%2Cdotnet) - [Returning Adaptive Card Previews in Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit?tabs=dotnet%2Cdotnet-1#bot-response-with-adaptive-card) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/link-unfurling.mdx ================================================ --- sidebar_position: 4 sidebar_label: 'Link Unfurling' title: 'Link Unfurling' summary: Enable your app to respond when users paste URLs by creating preview cards with additional information and actions. --- # 🔗 Link unfurling Link unfurling lets your app respond when users paste URLs into Teams. When a URL from your registered domain is pasted, your app receives the URL and can return a card with additional information or actions. This works like a search command where the URL acts as the search term. :::note Users can use link unfurling even before they discover or install your app in Teams. This is called [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling). In this scenario, your app will receive a `message.ext.anon-query-link` activity instead of the usual `message.ext.query-link`. ::: ## Setting up your Teams app manifest ### Configure message handlers ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "messageHandlers": [ { "type": "link", "value": { "domains": [ "www.test.com" ] } } ] } ] ``` ### How link unfurling works When a user pastes a URL from your registered domain (like `www.test.com`) into the Teams compose box, your app will receive a notification. Your app can then respond by returning an adaptive card that displays a preview of the linked content. This preview card appears before the user sends their message in the compose box, allowing them to see how the link will be displayed to others. ```mermaid flowchart TD A1["User pastes a URL (e.g., www\.test\.com) in Teams compose box"] B1([Microsoft Teams]) C1["Your App"] D1["Adaptive Card Preview"] A1 --> B1 B1 -->|Sends URL paste notification| C1 C1 -->|Returns card and preview| B1 B1 --> D1 %% Styling for readability and compatibility style B1 fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style C1 fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff style D1 fill:#F39C12,stroke:#D68910,stroke-width:2px,color:#ffffff ``` ## Implementing link unfurling ### Handle the query link event Handle link unfurling when a URL from your registered domain is submitted into the Teams compose box. ### Create the unfurl card ### User experience flow The link unfurling response includes both a full adaptive card and a preview card. The preview card appears in the compose box when a user pastes a URL: ![Screenshot showing a preview card for an unfurled URL in the Teams compose box.](/screenshots/link-unfurl-preview.png) The user can expand the preview card by clicking on the _expand_ button on the top right. ![Screenshot of Teams compose box with an outline around the unfurled link card labeled 'Adaptive Card'.](/screenshots/link-unfurl-card.png) The user can then choose to send either the preview or the full adaptive card as a message. ## Resources - [Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages) - [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/search-commands.mdx ================================================ --- sidebar_position: 2 sidebar_label: 'Search Commands' title: 'Search Commands' summary: Create search commands that allow users to search external systems and insert results as cards in Teams messages. --- # 🔍 Search commands Message extension search commands allow users to search external systems and insert the results of that search into a message in the form of a card. ## Search command invocation locations There are two different areas search commands can be invoked from: 1. Compose Area 2. Compose Box ### Compose Area and Box ![Screenshot of Teams with outlines around the 'Compose Box' (for typing messages) and the 'Compose Area' (the menu option next to the compose box that provides a search bar for actions and apps).](/screenshots/compose-area.png) ## Setting up your Teams app manifest To use search commands you have to define them in the Teams app manifest. Here is an example: ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "searchQuery", "context": [ "compose", "commandBox" ], "description": "Test command to run query", "title": "Search query", "type": "query", "parameters": [ { "name": "searchQuery", "title": "Search Query", "description": "Your search query", "inputType": "text" } ] } ] } ] ``` Here we are defining the `searchQuery` search (or query) command. ## Handle submission Handle the search query submission when the `searchQuery` search command is invoked. The search results include both a full adaptive card and a preview card. The preview card appears as a list item in the search command area: ![Screenshot of Teams showing a message extensions search menu open with list of search results displayed as preview cards.](/screenshots/preview-card.png) When a user clicks on a list item the dummy adaptive card is added to the compose box: ![Screenshot of Teams showing the selected adaptive card added to the compose box.](/screenshots/card-in-compose.png) To implement custom actions when a user clicks on a search result item, you can add the `tap` property to the preview card. This allows you to handle the click event with custom logic: ## Resources - [Search command](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command?tabs=Teams-toolkit%2Cdotnet) - [Just-In-Time Install](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/universal-actions-for-search-based-message-extensions#just-in-time-install) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/message-extensions/settings.mdx ================================================ --- sidebar_position: 3 sidebar_label: 'Settings' title: 'Settings' summary: Add configurable settings pages to your message extensions to allow users to customize app behavior. ignore: true --- import SettingsImgUrl from '@site/static/screenshots/settings.png'; # ⚙️ Settings You can add a settings page that allows users to configure settings for your app. The user can access the settings by right-clicking the app item in the compose box.
Settings This guide will show how to enable user access to settings, as well as setting up a page that looks like this: ![Settings Page](/screenshots/settings-page.png) ## 1. Update the Teams Manifest Set the `canUpdateConfiguration` field to `true` in the desired message extension under `composeExtensions`. ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "canUpdateConfiguration": true, ... } ] ``` ## 2. Serve the settings `html` page This is the code snippet for the settings `html` page: Save it in the `index.html` file in the same folder as where your app is initialized. You can serve it by adding the following code to your app: ## 3. Specify the URL to the settings page To enable the settings page, your app needs to handle the `message.ext.query-settings-url` activity that Teams sends when a user right-clicks the app in the compose box. Your app must respond with the URL to your settings page. Here's how to implement this: ## 4. Handle Form Submission When a user submits the settings form, Teams sends a `message.ext.setting` activity with the selected option in the `activity.value.state` property. Handle it to save the user's selection: ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/observability/README.mdx ================================================ --- title: Observability sidebar_position: 8 summary: Monitor and track your Teams app performance with logging, middleware, and observability tools. suppressLanguageIncludeWarning: true --- # Observability ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/observability/_category_.json ================================================ { "label": "Observability", "position": 8, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/observability/logging.mdx ================================================ --- sidebar_position: 2 sidebar_label: '🗃️ Custom Logger' title: '🗃️ Custom Logger' summary: Configure custom loggers in your Teams app to control log levels and output destinations. --- # 🗃️ Custom Logger The `App` will provide a default logger, but you can also provide your own. The default `Logger` instance will be set to from the package. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/observability/middleware.mdx ================================================ --- sidebar_position: 1 title: 'Middleware' summary: Create middleware for logging, validation, and other cross-cutting concerns using the app.use method. --- # Middleware Middleware is a useful tool for logging, validation, and more. You can easily register your own middleware using the method. Below is an example of a middleware that will log the elapse time of all handlers that come after it. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/server/_category_.json ================================================ { "label": "Server", "position": 9, "collapsible": true, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/server/http-server.mdx ================================================ --- sidebar_position: 2 title: Self-Managing Your Server sidebar_label: Self-Managing Your Server languages: ['typescript', 'python'] summary: How to self-manage the HTTP server — bring your own Express, FastAPI, or any framework by implementing the HttpServerAdapter interface. --- # Self-Managing Your Server By default, `app.start()` spins up an HTTP server, registers the Teams endpoint, and manages the full lifecycle for you. Under the hood, the SDK uses as its built-in HTTP framework. But if you need to self-manage your server — because you have an existing app, need custom server configuration (TLS, workers, middleware), or use a different HTTP framework — the SDK supports that through the `HttpServerAdapter` interface. ## How It Works The SDK splits HTTP handling into two layers: - **HttpServer** handles Teams protocol concerns: JWT authentication, activity parsing, and routing to your handlers. - **HttpServerAdapter** handles framework concerns: translating between your HTTP framework's request/response model and the SDK's pure handler pattern. ```mermaid flowchart LR Teams["Teams Service"] -->|HTTP POST| Adapter["HttpServerAdapter
(your framework)"] Adapter -->|"{ body, headers }"| HttpServer["HttpServer
(auth + parse)"] HttpServer --> Handlers["Your App Handlers"] Handlers -->|"{ status, body }"| HttpServer HttpServer --> Adapter Adapter -->|"HTTP Response"| Teams ``` The adapter interface is intentionally simple — implement `registerRoute` and the SDK handles the rest. ## The Adapter Interface - **`registerRoute`** — Required. Routes are registered dynamically (`/api/messages`, `/api/functions/{name}`, etc.). - **`serveStatic`** — Optional. Only needed for tabs or static pages. - **`start` / `stop`** — Optional. Omit when you manage the server lifecycle yourself. ## Self-Managing Your Server To add Teams to an existing server: 1. Create your server with your own routes and middleware. 2. Wrap it in an adapter (or use the built-in one with your server instance). 3. Call `app.initialize()` — this registers the Teams routes on your server. Do **not** call `app.start()`. 4. Start the server yourself. ## Using a Different Framework If you use a framework other than the built-in default, implement the adapter interface for your framework. The core work is in `registerRoute` — translate incoming requests to `{ body, headers }`, call the handler, and write the response back. Since you manage the server lifecycle yourself, `start`/`stop` aren't needed. And `serveStatic` is only required if you serve tabs or static pages. ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/server/static-pages.mdx ================================================ --- sidebar_position: 1 sidebar_label: Static Pages title: Static Pages summary: Shows how to host web apps. --- # Static Pages The `App` class lets you host web apps in the agent. This can be used for an efficient inner loop when building a complex app using Microsoft 365 Agents Toolkit, as it lets you build, deploy, and sideload both an agent and a Tab app inside of Teams in a single step. It's also useful in production scenarios, as it makes it straight-forward to host a simple experience such as an agent configuration page or a Dialog. To host a static tab web app, call the function and provide an app name and a path to a folder containing an `index.html` file to be served up. This registers a route that is hosted at . ## Additional resources ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/README.mdx ================================================ --- title: Tabs sidebar_position: 6 summary: Build Teams tab apps with Graph integration, authentication, and remote agent function calling capabilities. languages: ['typescript', 'csharp'] --- # Tabs Tabs are host-aware webpages embedded in Microsoft Teams, Outlook, and Microsoft 365. Tabs are commonly implemented as Single Page Applications that use the Teams [JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) (TeamsJS) to interact with the app host. ## Resources - [Tabs overview](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/what-are-tabs?tabs=personal) - [Teams JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) - [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview) - [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) - [Nested App Authentication (NAA)](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/authentication/nested-authentication) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/_category_.json ================================================ { "label": "Tabs", "position": 6, "collapsible": true, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/app-options.mdx ================================================ --- sidebar_position: 3 title: 'App Options' summary: Configure app settings for observability, MSAL authentication, and remote agent function calling. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # App Options The app options offer various settings that you can use to customize observability, Microsoft Authentication Library (MSAL) configuration, and remote agent function calling. Each setting is optional, with the app using a reasonable default as needed. ## Logger If no logger is specified in the app options, the app will create a [ConsoleLogger](../observability/logging). You can however provide your own logger implementation to control log level and destination. ```typescript import { App } from '@microsoft/teams.client'; import { ConsoleLogger } from '@microsoft/teams.common'; const app = new App(clientId, { logger: new ConsoleLogger('myTabApp', { level: 'debug' }), }); await app.start(); ``` ## Remote API options The remote API options let you control which endpoint that `app.exec()` make a request to, as well as the default resource name to use when requesting an MSAL token to attach to the request. ### Base URL The `baseUrl` value is used to provide the URL where the remote API is hosted. This can be omitted if the tab app is hosted on the same domain as the remote agent. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId, { remoteApiOptions: { baseUrl: 'https://agent1.contoso.com', }, }); await app.start(); // this requests a token for 'api:///access_as_user' and attaches // that to a request to https://agent1.contoso.com/api/functions/my-function await app.exec('my-function'); ``` ### Remote app resource The `remoteAppResource` value is used to control the default resource name used when building a token request for the Entra token to include when invoking the function. This can be omitted if the tab app and the remote agent app are in the same AAD app, but should be provided if they're in different apps or the agent requires scopes for a different resource than the default `api:///access_as_user`. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId, { remoteApiOptions: { baseUrl: 'https://agent1.contoso.com', remoteAppResource: 'api://agent1ClientId', }, }); await app.start(); // this requests a token for 'api://agent1ClientId/access_as_user' and attaches that // to a request to https://agent1.contoso.com/api/functions/my-function await app.exec('my-function'); ``` ## MSAL options The MSAL options let you control how the Microsoft Authentication Library (MSAL) is initialized and used, and how the user is prompted for scope consent as the app starts. ### MSAL instance and configuration You have three options to control the MSAL instance used by the app. - Provide a pre-configured and pre-initialized MSAL IPublicClientApplication. - Provide a custom MSAL configuration for the app to use when creating an MSAL IPublicClientApplication instance. - Provide neither, and let the app create IPublicClientApplication from a default MSAL configuration. #### Default behavior If the app options contain neither an MSAL instance nor an MSAL configuration, the app constructs a simple MSAL configuration that is suitable for multi-tenant apps and that connects the MSAL logger callbacks to the app logger. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId); await app.start(); // app.msalInstance is now available, and any logging is forwarded from // MSAL to the app.log instance. ``` #### Providing a custom MSAL configuration MSAL offers a rich set of configuration options, and you can provide your own configuration as an app option. ```typescript import * as msal from '@azure/msal-browser'; import { App } from '@microsoft/teams.client'; const configuration: msal.Configuration = { /* custom MSAL configuration options */ }; const app = new App(clientId, { msalOptions: { configuration } }); await app.start(); ``` #### Providing a pre-configured MSAL IPublicClientApplication MSAL cautions against an app using multiple IPublicClientApp instances at the same time. If you're already using MSAL, you can provide a pre-created MSAL instance to use as an app option. ```typescript import * as msal from '@azure/msal-browser'; import { App } from '@microsoft/teams.client'; const msalInstance = await msal.createNestablePublicClientApplication(/* custom MSAL configuration */); await msalInstance.initialize(); const app = new App(clientId, { msalOptions: { msalInstance } }); await app.start(); ``` If you need multiple app instances in order to call functions in several agents, you can re-use the MSAL instance from one as you construct another. ```typescript import { App } from '@microsoft/teams.client'; // let app1 create & initialize an MSAL IPublicClientApplication const app1 = new App(clientId, { remoteApiOptions: { baseUrl: 'https://agent1.contoso.com', remoteAppResource: 'api://agent1AppClientId', }, }); await app1.start(); // let app2 re-use the MSAL IPublicClientApplication from app1 const app2 = new App(clientId, { remoteApiOptions: { baseUrl: 'https://agent2.contoso.com', remoteAppResource: 'api://agent2AppClientId', }, msalOptions: { msalInstance: app1.msalInstance }, }); ``` ### Scope consent pre-warming The MSAL options let you control whether and how the user is prompted to give the app permission for any necessary scope as the app starts. This option can be used to reduce the number of consent prompts the user sees while using the app, and to help make sure the app gets consent for the resource it needs to function. With this option, you can either pre-warm a specific set of scopes or disable pre-warming altogether. If no setting is provided, the default behavior is to prompt the user for the Graph scopes listed in the app manifest, unless they've already consented to at least on Graph scope. For more details on how and when to prompt for scope consent, see the [Graph](./graph) documentation. #### Default behavior If the app is started without specifying any option to control scope pre-warming, the `.default` scope is pre-warmed. This means that in a first-run experience, the user would be prompted to consent for all Graph permissions listed in the app manifest. However, if the user has consented to at least one Graph permission, any one at all, no prompt appears. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId); // if the user hasn't already given consent for any scope at // all, this will prompt them await app.start(); ``` :::info The user can decline the prompt and the app will still continue to run. However, the user will again be prompted next time they launch the app. ::: #### Pre-warm a specific set of scopes If your app requires a specific set of scopes in order to run well, you can list those in the set of scopes to pre-warm. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId, { msalOptions: { prewarmScopes: ['User.Read', 'Chat.ReadBasic'] }, }); // if the user hasn't already given consent for each listed scope, // this will prompt them await app.start(); ``` :::info The user can decline the prompt and the app will still continue to run. However, the user will again be prompted next time they launch the app. ::: #### Disabling pre-warming Scope pre-warming can be disabled if needed. This is useful if your app doesn't use graph APIs, or if you want more control over the consent prompt. ```typescript import { App } from '@microsoft/teams.client'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App(clientId, { msalOptions: { prewarmScopes: false }, }); // this will not raise any consent prompt await app.start(); // this will prompt for the '.default' scope if the user hasn't already // consented to any scope const top10Chats = await app.graph.call(endpoints.chats.list, { $top: 10 }); ``` :::info Even if pre-warming is disabled and the user is not prompted to consent, a prompt for the `.default` scope will appear when invoking any graph API. ::: ## References [MSAL Configuration](https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-application-configuration) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/functions/README.mdx ================================================ --- sidebar_position: 5 title: 'Functions' summary: Details on how to register REST endpoints that can be called from Tab apps. languages: ['typescript', 'csharp'] --- # Functions ## Function context ## Additional resources ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/functions/function-calling.mdx ================================================ --- sidebar_position: 1 title: 'Executing Functions' summary: Call remote agent functions from tab apps with authentication and custom headers using the exec() method. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # Executing Functions The client App exposes an `exec()` method that can be used to call functions implemented in an agent created with this SDK. The function call uses the `app.http` client to make a request, attaching a bearer token created from the `app.msalInstance` MSAL public client application, so that the remote function can authenticate and authorize the caller. The `exec()` method supports passing arguments and provides options to attach custom request headers and/or controlling the MSAL token scope. ## Invoking a remote function When the tab app and the remote agent are deployed to the same location and in the same AAD app, it's simple to construct the client app and call the function. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId); await app.start(); // this requests a token for 'api:///access_as_user' and attaches // that to an HTTP POST request to /api/functions/my-function const result = await app.exec('my-function'); ``` If the deployment is more complex, the [AppOptions](../app-options) can be used to influence the URL as well as the scope in the token. ## Function arguments Any argument for the remote function can be provided as an object. ```typescript const args = { arg1: 'value1', arg2: 'value2' }; const result = await app.exec('my-function', args); ``` ## Request headers By default, the HTTP request will include a header with a bearer token as well as headers that give contextual information about the state of the app, such as which channel or team or chat or meeting the tab is active in. If needed, you can add additional headers to the `requestHeaders` option field. This may be handy to provide additional context to the remote function, such as a logging correlation ID. ```typescript const requestHeaders = { 'x-custom-correlation-id': 'aaaa0000-bb11-2222-33cc-444444dddddd', }; // custom headers when the function does not take arguments const result = await app.exec('my-function', undefined, { requestHeaders }); // custom headers when the function takes arguments const args = { arg1: 'value1', arg2: 'value2' }; const result = await app.exec('my-other-function', args, { requestHeaders }); ``` ## Request bearer token By default, the HTTP request will include a header with a bearer token acquired by requesting an `access_as_user` permission. The resource used for the request depends on the `remoteApiOptions.remoteAppResource` [AppOption](../app-options). If this app option is not provided, the token is requested for the scope `api:///access_as_user`. If this option is provided, the token is requested for the scope `/access_as_user`. When calling a function that requires a different permission or scope, the `exec` options let you override the behavior. To specify a custom permission, set the permission field in the `exec` options. ```typescript // with this option, the exec() call will request a token for either // api:///my_custom_permission or // /my_custom_permission, // depending on the app options used. const options = { permission: 'my_custom_permission', }; // custom permission when the function does not take arguments const result = await app.exec('my-function', undefined, options); // custom permission when the function takes arguments const args = { arg1: 'value1', arg2: 'value2' }; const result = await app.exec('my-other-function', args, options); ``` Sometimes you may need even more control. You might for need a scope for a different resource than your default when calling a particular remote agent function. In these cases you can provide the exact token request object you need as part of the `exec` options. ```typescript // with this option, the exec() call will request a token for exactly // api://my-custom-resources/my_custom_scope, regardless of which app // options were used to construct the app. const options = { msalTokenRequest: { scopes: ['api://my-custom-resources/my_custom_scope'], }, }; // custom token request when the function does not take arguments const result = await app.exec('my-function', undefined, options); // custom token request when the function takes arguments const args = { arg1: 'value1', arg2: 'value2' }; const result = await app.exec('my-other-function', args, options); ``` ## Ensuring user consent The `exec()` function supports incremental, just-in-time consent such that the user is prompted to consent during the `exec()` call, if they haven't already consented earlier. If you find that you'd rather test for consent or request consent before making the `exec()` call, the `hasConsentForScopes` and `ensureConsentForScopes` can be used. More details about those are given in the [Graph](../graph) section. ## References - [Graph API overview](https://learn.microsoft.com/en-us/graph/api/overview) - [Graph API permissions overview](https://learn.microsoft.com/en-us/graph/permissions-reference) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/getting-started.mdx ================================================ --- sidebar_position: 1 title: 'Getting Started' summary: Set up new tab app projects or add Teams client capabilities to existing tab applications. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # Getting started To use this package, you can either set up a new project using the Teams CLI, or add it to an existing tab app project. ## Setting up a new project The Teams CLI contains a Microsoft 365 Agents Toolkit configuration and a template to easily scaffold a new tab app with a callable remote function. To set this up, first install the Teams CLI as outlined in the [Quickstart](../../getting-started/quickstart) guide. Then, create the app by running: ```sh npx @microsoft/teams.cli@latest new typescript my-first-tab-app --atk embed --template tab ``` When the app is created, you can use the Agents Toolkit to run and debug it inside of Teams from your local machine, same as for any other Agents Toolkit tab app. ## Adding to an existing project This package is set up to integrate well with existing Tab apps. The main consideration is that the AAD app must be configured to support Nested App Authentication (NAA). Otherwise it will not be possible to acquire the bearer token needed to call Microsoft Graph APIs or remote agent functions. After verifying that the app is configured for NAA, simply use your package manager to add a dependency on `@microsoft/teams.client` and then proceed with [Starting the app](./using-the-app). If you're already using a current version of TeamsJS, that's fine. This package works well with TeamsJS. If you're already using Microsoft Authentication Library (MSAL) in an NAA enabled app, that's great! The [App options](./app-options) page shows how you can use a single common MSAL instance. ## Resources - [Running and debugging local apps in Agents Toolkit](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-local?tabs=Windows) - [Configuring an app for Nested App Authentication](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/authentication/nested-authentication#configure-naa) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/graph.mdx ================================================ --- sidebar_position: 4 title: 'Microsoft Graph' summary: Access Microsoft Graph APIs with type-safe client and manage user consent for permissions. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # Microsoft Graph Client The client App exposes a `graph` property that gives type-safe access to Microsoft Graph functions. When graph functions are invoked, the app attaches an MSAL bearer token to the request so that the call can be authenticated and authorized. ## Invoking Graph functions After constructing and starting an App instance, you can invoke any graph function by using the `app.graph` client. ```typescript import { App } from '@microsoft/teams.client'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App(clientId); await app.start(); const top10Chats = await app.graph.call(endpoints.chats.list, { $top: 10 }); ``` For best result, it's wise to ensure that the user has consented to a permission required by the graph API before attempting to invoke it. Otherwise, the call is likely to be rejected by the graph server. ## Graph APIs and permissions Different graph APIs have different permission requirements. The app developer should make sure that consent is granted before invoking a graph API. To help request and test for consent, the client App offers three methods: - Pre-warming while starting the app. - Requesting consent if not already granted. - Testing for consent without prompting. ### Pre-warming while starting the app The App constructor takes an option that lets you control how scope consent is requested while starting the app. For more details on this option, see the [App options](./app-options) documentation. ### Requesting consent if not already granted The app provides an `ensureConsentForScopes` method that tests if the user has consented to a certain set of scopes and prompts them if consent isn't yet granted. The method returns a promise that resolves to true if the user has already provided consent to all listed scopes; and to false if the user declines the prompt. This method is useful for building an incremental, just-in-time, consent model, or to fully control how consent is pre-warmed. ```typescript import { App } from '@microsoft/teams.client'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App(clientId, { msalOptions: { prewarmScopes: ['User.Read'] }, }); // this will prompt for the User.Read scope if not already granted await app.start(); // this will prompt for Chat.ReadBasic if not already granted const canReadChat = await app.ensureConsentForScopes(['Chat.ReadBasic']); if (canReadChat) { const top10Chats = await app.graph.call(endpoints.chats.list, { $top: 10 }); // ... do something useful ... } ``` #### Testing for consent without prompting The app also provides a `hasConsentForScopes` method to test for consent without raising a prompt. This is handy to enable or disable features based on user choice, or to provide friendly messaging before raising a prompt with `ensureConsentForScopes`. ```typescript import { App } from '@microsoft/teams.client'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App(clientId); // this will prompt for the '.default' scope if the user hasn't already // consented to any scope await app.start(); // this will not raise a prompt under any circumstance const canReadChat = await app.hasConsentForScopes(['Chat.ReadBasic']); if (canReadChat) { const top10Chats = await app.graph.call(endpoints.chats.list, { $top: 10 }); // ... do something useful ... } ``` ## References - [Graph API overview](https://learn.microsoft.com/en-us/graph/api/overview) - [Graph API permissions overview](https://learn.microsoft.com/en-us/graph/permissions-reference) ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/tabs/using-the-app.mdx ================================================ --- sidebar_position: 2 title: 'Using the App' summary: Initialize and use the Teams client App to call Graph APIs and remote agent functions. languages: ['typescript'] suppressLanguageIncludeWarning: true --- # Using The App The `@microsoft/teams.client` App class helps solve common challenges when building Single Page Applications hosted in Microsoft Teams, Outlook, and Microsoft 365. It is the client-side counterpart to the `@microsoft/teams.app` App that you can use to build AI agents. These two App classes are designed to work well together. For instance, when you use the `@microsoft/teams.app` App to expose a server-side function, you can then use the `@microsoft/teams.client` App `exec` method to easily invoke that function, as the client-side app knows how to construct an HTTP request that the server-side app can process. It can issue a request to the right URL, with the expected payload and contextual headers. The client-side app even includes a bearer token that the server side app uses to authenticate the caller. # Starting the app To use the `@microsoft/teams.client` package, you first create an App instance and then call `app.start()`. ```typescript import { App } from '@microsoft/teams.client'; const app = new App(clientId); await app.start(); ``` The app constructor strives to make it easy to get started on a new app, while still being flexible enough that it can integrate easily with existing apps. The constructor takes two arguments: a required app client ID, and an optional `AppOptions` argument. The app client ID is the AAD app registration **Application (client) ID**. The options can be used to customize observability, Microsoft Authentication Library (MSAL) configuration, and remote agent function calling. For more details on the app options, see the [App options](./app-options) page. ## What happens during start The app constructor does the following: - it creates an app logger, if none is provided in the app options. - it creates an http client used to call the remote agent. - it creates a graph client that can be used as soon as the app is started. The `app.start()` call does the following: - it initializes TeamsJS. - it creates an MSAL instance, if none is provided in the app options. - it connects the MSAL instance to the graph client. - it prompts the user for MSAL token consent, if needed and if pre-warming is not disabled through the app options. ## Using the app When the `app.start()` call has completed, you can use the app instance to call Graph APIs and to call remote agent functions using the `exec()` function, or directly by using the `app.http` HTTP client. TeamsJS is now initialized, so you can interact with the hosting app. The `app.msalInstance` is now populated, in case you need to use the same MSAL for other purposes. ```typescript import * as teamsJs from '@microsoft/teams-js'; import { App } from '@microsoft/teams.client'; import * as endpoints from '@microsoft/teams.graph-endpoints'; const app = new App(clientId); await app.start(); // you can now get the TeamsJS context... const context = await teamsJs.app.getContext(); // ...call Graph end points... const presenceResult = await app.graph.call(endpoints.me.presence.get); // ...and call remote agent functions... const agentResult = await app.exec('hello-world'); ``` ================================================ FILE: teams.md/src/pages/templates/in-depth-guides/user-authentication.mdx ================================================ --- sidebar_position: 4 sidebar_label: 🔒 User Authentication title: 🔒 User Authentication summary: API guide to implement User Authentication with SSO in Teams Apps. --- # 🔒 User Authentication At times agents must access secured online resources on behalf of the user, such as checking email, checking on flight status, or placing an order. To enable this, the user must authenticate their identity and grant consent for the application to access these resources. This process results in the application receiving a token, which the application can then use to access the permitted resources on the user's behalf. :::info This is an advanced guide. It is highly recommended that you are familiar with [Teams Core Concepts](/teams/core-concepts) before attempting this guide. ::: :::warning User authentication does not work with the developer tools setup. You have to run the app in Teams. Follow these [instructions](/typescript/getting-started/running-in-teams#debugging-in-teams) to run your app in Teams. ::: :::info It is possible to authenticate the user into [other auth providers](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-identity-providers?view=azure-bot-service-4.0&tabs=adv2%2Cga2#other-identity-providers) like Facebook, Github, Google, Dropbox, and so on. ::: Once you have configured your Azure Bot resource OAuth settings, as described in the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0), add the following code to your `App`: ## Project Setup ### Create an app with the `graph` template :::tip Skip this step if you want to add the auth configurations to an existing app. ::: ### Add Agents Toolkit auth configuration Open your terminal with the project folder set as the current working directory and run the following command: ```sh npx @microsoft/teams.cli config add atk.oauth ``` The `atk.oauth` configuration is a basic setup for Agents Toolkit along with configurations to authenticate the user with Microsoft Entra ID to access Microsoft Graph APIs. This [CLI](/developer-tools/cli) command adds configuration files required by Agents Toolkit, including: - Azure Application Entra ID manifest file `aad.manifest.json`. - Azure bicep files to provision Azure bot in `infra/` folder. :::info Agents Toolkit, in the debugging flow, will deploy the `aad.manifest.json` and `infra/azure.local.bicep` file to provision the Application Entra ID and Azure bot with oauth configurations. ::: ## Configure the OAuth connection :::tip Make sure you use the same name you used when creating the OAuth connection in the Azure Bot Service resource. ::: :::note In many templates, `graph` is the default name of the OAuth connection, but you can change that by supplying a different connection name in your app configuration. ::: ## Signing In :::note This uses the Single Sign-On (SSO) authentication flow. To learn more about all the available flows and their differences see the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0). ::: You must call the `signin` method inside your route handler, for example: to signin when receiving the `/signin` message: ## Subscribe to the SignIn event You can subscribe to the `signin` event, that will be triggered once the OAuth flow completes. ## Start using the graph client From this point, you can use the `IsSignedIn` flag and the `userGraph` client to query graph, for example to reply to the `/whoami` message, or in any other route. :::note The default OAuth configuration requests the `User.ReadBasic.All` permission. It is possible to request other permissions by modifying the App Registration for the bot on Azure. ::: ## Signing Out You can signout by calling the `signout` method, this will remove the token from the User Token service cache ## Handling Sign-In Failures When using SSO, if the token exchange fails Teams sends a `signin/failure` invoke activity to your app. The SDK includes a built-in default handler that logs a warning with actionable troubleshooting guidance. You can optionally register your own handler to customize the behavior: :::tip The most common failure codes are `installedappnotfound` (bot app not installed for the user) and `resourcematchfailed` (Token Exchange URL doesn't match the Application ID URI). See [SSO Setup - Troubleshooting](/teams/user-authentication/sso-setup#troubleshooting-sso) for a full list of failure codes and troubleshooting steps. ::: ## Resources [User Authentication Basics](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0) ================================================ FILE: teams.md/src/pages/templates/migrations/README.mdx ================================================ --- sidebar_position: 4 title: Migrations languages: ['typescript', 'python'] summary: Migration guides for transitioning from older versions and frameworks to Teams SDK. llms: ignore suppressLanguageIncludeWarning: true --- # Migrations Migration guides for transitioning from older versions and frameworks to Teams SDK. ================================================ FILE: teams.md/src/pages/templates/migrations/_category_.json ================================================ { "collapsed": true, "label": "Migrations" } ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/README.mdx ================================================ --- sidebar_position: 1 sidebar_label: From BotBuilder title: From BotBuilder languages: ['typescript', 'csharp', 'python'] summary: Migration guide from BotBuilder to Teams SDK, including the BotBuilder plugin for compatibility with existing activity handlers and adapters. llms: ignore --- # From BotBuilder This new iteration of Teams SDK has been rebuilt from the ground up. To ease the migration process, we've introduced a plugin that allows you to continue using BotBuilder components like `ActivityHandler` and `CloudAdapter` to receive, process and send activities within the new Teams SDK abstractions. ## Why a Plugin? The plugin exists to bridge BotBuilder and the new Teams SDK, letting developers keep their existing BotBuilder activity handlers while gradually moving to the new Teams SDK App handlers. It enables incremental migration and smooth adoption of new SDK features. ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/_category_.json ================================================ { "label": "BotBuilder Migration", "position": 1, "collapsed": true } ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/integration.mdx ================================================ --- sidebar_position: 1 languages: ['typescript', 'csharp', 'python'] title: Using the BotBuilder Plugin summary: How to migrate BotBuilder adapters to Teams SDK plugins for handling bot communication and middleware. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Using the BotBuilder Plugin # Adapters A BotBuilder `CloudAdapter` is responsible for managing communication between a bot and its users. It serves as the entry point for incoming activities and forwards them to the registered `ActivityHandler` for processing. You can customize the adapter to add middleware for logging, authentication, and define error handling. The `BotBuilderPlugin` provided within the Teams SDK, connects the SDK with the BotBuilder framework. It can either use an existing `CloudAdapter` or create a new default one, allowing activities to be processed through BotBuilder while still handling events via the Teams SDK App framework. # Activity Handlers The BotBuilder `ActivityHandler` contains the actual bot logic for processing messages or events similar to how the Teams SDK `App` routes messages and events. You can override any number of methods, such as or , to handle different activity types. # Turn Context Each incoming activity is wrapped in a `TurnContext`, which represents the context of a single turn in the conversation. TurnContext provides access to: - The incoming activity (message, event). - Services for sending responses back to the user. - Conversation, user, and channel metadata. Teams SDK has for the same purpose. # How it all comes together The `CloudAdapter` creates the `TurnContext`, and the `ActivityHandler` uses it to read the activity and send responses. With the `BotBuilderPlugin`, when a message or activity is received: 1. The BotBuilder ActivityHandler runs first, handling the activity according to standard Bot Framework logic. 2. The Teams SDK app based activity handlers execute afterward, allowing Teams SDK logic to execute. :::info This snippet shows how to use the `BotBuilderPlugin` to send and receive activities using botbuilder instead of the default Teams SDK http plugin. ::: In this example: - defines a `CloudAdapter` to handle incoming activities, and can include middleware support or error handling. - defines the `ActivityHandler` and contains the core bot logic, handling incoming messages and sending responses via the `TurnContext`. - sets up a Teams SDK `app` and registers the `BotBuilderPlugin` with your adapter and activity handler. It also defines a native Teams SDK activity handler that responds to messages. In the ouptut below, The first line comes from the BotBuilder ActivityHandler. The second line comes from the Teams SDK message activity handler. This shows that both handlers can process the same message sequentially when using the BotBuilder Plugin. This strategy can now be used to incrementally migrate from BotBuilder to the Teams SDK. ``` hi from botbuilder... hi from teams... ``` ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/proactive-activities.mdx ================================================ --- sidebar_position: 2 sidebar_label: Proactive Activities title: Proactive Activities languages: ['typescript', 'csharp', 'python'] summary: Migrate from BotBuilder's complex conversation reference handling to Teams SDK's simple conversation ID-based proactive messaging. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Proactive Activities The BotBuilder proactive message flow requires storing a conversation reference. In Teams SDK, we expose a method in the `App` class, almost identical to the one passed into our activity handlers through our context. This method accepts a , so storing just that is enough! ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/sending-activities.mdx ================================================ --- sidebar_position: 1 sidebar_label: Sending Activities title: Sending Activities languages: ['typescript', 'csharp', 'python'] summary: Migrate from BotBuilder's TurnContext activity sending to Teams SDK's simplified send method with better Adaptive Card support. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Sending Activities BotBuilder's pattern for sending activities via its `TurnContext` is similar to that in Teams SDK, but one key difference is that sending adaptive cards doesn't require constructing the entire activity yourself. ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/the-api-client.mdx ================================================ --- sidebar_position: 3 sidebar_label: The API Client title: The API Client languages: ['typescript', 'csharp', 'python'] summary: Replace BotBuilder's static TeamsInfo class with Teams SDK's injected ApiClient for cleaner API interactions. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # The API Client BotBuilder exposes a static class `TeamsInfo` that allows you to query the api. In Teams SDK we pass an instance of our `ApiClient` into all our activity handlers through the context. :::tip The Teams SDK `ApiClient` uses a fluent API pattern that makes it easier to discover available methods through IDE autocompletion. ::: ## Mapping TeamsInfo APIs to Teams SDK ApiClient Methods The following table shows common BotBuilder `TeamsInfo` methods and their equivalent Teams SDK `ApiClient` methods: ================================================ FILE: teams.md/src/pages/templates/migrations/botbuilder/user-authentication.mdx ================================================ --- sidebar_position: 4 sidebar_label: User Authentication title: User Authentication languages: ['typescript', 'csharp', 'python'] summary: Migrate from BotBuilder's complex OAuthPrompt dialogs to Teams SDK's simple signin/signout methods. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # User Authentication BotBuilder uses its `dialogs` for authentication via the `OAuthPrompt`. Teams SDK doesn't have any equivalent feature for dialogs, but we do support auth flows in our own way via our methods. ================================================ FILE: teams.md/src/pages/templates/migrations/slack-bolt.mdx ================================================ --- sidebar_position: 2 title: Porting your Slack Bolt bot languages: ['typescript'] summary: Migration & port guide from Slack Bolt to Teams SDK, highlighting the key changes and upgrade steps. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Porting your Slack Bolt bot This guide will help you migrate or port your existing Slack Bolt application to the Teams SDK. We'll cover the key conceptual similarities and differences between Slack and Teams APIs, and provide code examples to help you port things over in no time. Let's get started! ## Introduction Similar to Slack Bolt, the Teams SDK is designed to interface with the Teams backend APIs for building conversational applications. Both SDKs provide abstractions for handling incoming events, messages, and interactions, as well as sending responses back to users. If you already have a Slack bot, the concepts should feel familiar. However, there are some key differences, such as with app installation. In Slack, apps are installed to workspaces via OAuth, whereas in Teams they are installed via the Teams App Store. Additionally, Teams apps can be installed personally by individual users, or to collaborative scopes like group chats, channels, and meetings. Let's take a look at some similarities and differences between Slack and Teams concepts: | Concept | Teams | Slack | | ------------- | ------------- | ------------- | | **Installation** | Installed via app store on per-scope basis | Installed via OAuth to Workspace | | **Quickstart** | New projects created via Agent Toolkit CLI, Visual Studio extension, or VS Code extension. | New projects created via Slack CLI. | | **App manifest** | Authored via JSON, Agent Toolkit, and/or via Teams Developer Portal. Must also setup Azure Bot and Azure App Registration resources, which can be done via Azure Portal or Agent Toolkit. | Authored via JSON, YAML, or app management page. | | **Messaging endpoint** | Set in Azure Bot resource. | Set in Slack app manifest. | | **App authentication** | Entra App Registration is authorized during Teams app installation. Teams SDK fetches Entra app token internally when sending messages. Core app features like messaging use this auth type internally within the SDK. | App stores Slack bot token after user authorizes application-delegated scopes (e.g.,`incoming-webhook,commands`). Core app features like messaging use this auth type internally within the SDK. | | **User authentication for REST APIs** | User Entra tokens can be obtained using Teams SSO. Graph REST APIs are integrated into the Teams SDK. Tokens are stored and refreshed by Azure Bot Token service. | User Slack tokens can be obtained using OAuth 2.0. Slack REST APIs are integrated into Slack Bolt. Tokens must be stored and refreshed by application. | | **Authentication with external services** | Obtain user access tokens for external services using OAuth 2.0. Tokens are stored and refreshed by Azure Bot Token service. | Users authenticate to external services using OAuth 2.0, perhaps initiated via account binding (see below). Tokens must be stored and refreshed by application. | | **Account linking** | `Activity` events include `Activity.from.id`, which is the user's AAD object ID. If you authenticate your external service with OAuth 2.0, these accounts are implicitly bound via the Azure Token Service, but you can also follow a similar flow as what Slack recommends. | Slack recommends following their [Binding accounts across services](https://docs.slack.dev/authentication/binding-accounts-across-services) guide. | | **Cards** | Rich UI elements in messages using Adaptive Cards. | Rich UI elements in messages using Block Kit. | | **Files** | Files can be attached or downloaded using SharePoint / OneDrive Graph APIs. | Files can be attached or downloaded via Slack's files APIs. | | **Targeted messages** | Teams does not currently support targeted messages. | Slack supports targeted ephemeral messages. | | **Slash commands** | Teams supports [slash commands](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/create-a-bot-commands-menu) that are declared in the app manifest. Unlike Slack, commands are sent as messages and thus are visible to all users in the conversation. Listen for new commands using `app.message` handler, either via `app.message('/command')` or `app.message(regexp)`. | Slack Bolt has a dedicated `app.command` handler for commands in the Slack app manifest. Slash commands are not displayed to other users in collaborative contexts. | | **Workflows** | Teams Workflows is not integrated with the Teams SDK. To integrate with Teams Workflows, you must create a [custom Power Platform Connector](https://learn.microsoft.com/connectors/custom-connectors/). | Slack Workflows are integrated with Slack Bolt. | | **UI dialogs** | Adaptive Cards can include actions that open [UI dialogs](../in-depth-guides/dialogs) with an embedded website or another adaptive card. Dialogs must be opened via an adaptive card action and thus cannot be opened directly via a slash command. | BlockKit UI modals can be opened via slash commands (using `client.views.open`) or BlockKit actions. | | **AI strategy** | Teams has unique AI-native features for things like user feedback, AI-generated labels, prompt suggestions, streaming, and citations. We also feature an optional `ChatPrompt` class to simplify integrating LLMs into your bot. Leverage grounded search via the [Microsoft 365 Copilot Retrieval Graph API](https://learn.microsoft.com/microsoft-365-copilot/extensibility/api/ai-services/retrieval/overview). AI features are generally designed for use in any conversation type. | Slack has a dedicated `Assistant` class for AI interactions in a dedicated agent side panel view, which differs from Teams's strategy of using existing bot interaction patterns. Can still use AI in other conversation types using standard bot APIs. Can use Slack Data Access API for grounded search. | | **AI user feedback** | User feedback buttons are natively rendered in Teams with dedicated APIs for handling feedback. After user gives positive or negative feedback, a modal is opened where additional information (e.g., plain text response) can be captured. | Slack uses a dedicated `feedback_buttons` BlockKit element type and `app.action('feedback')` for attaching user feedback (positive vs. negative) to messages. | ## Configuring your application First, setup a new Teams application, as shown in [Teams Integration](../../../../docs/main/teams/README.md). ## Installing Teams SDK ## Configure application ## Migrate message handlers In Slack, there are message handlers for events with different subtypes (e.g., undefined subtype is a regular message, `event.subtype == 'file_share'` is a file share message, etc.). In Teams, there are different `Activity` handers for different types of events that are enumerated via the `ActivityTypes` enum (e.g., `app.activity(ActivityTypes.Message)`), with some `Activity` types having tailored APIs within the SDK (e.g., `app.message`). These concepts are roughly similar, though the naming conventions and syntax differ. ### Message handlers ## BlockKit -> Adaptive Cards To include Rich UI in messages sent by your bot, Slack's Block Kit is equivalent to Teams's Adaptive Cards. Learn more in the [Adaptive Cards guide](../in-depth-guides/adaptive-cards). ## User authentication There are two primary types of user authentication for Teams and Slack: authentication for Slack & Graph REST APIs, and authentication for external services. Let's take a look at each of these in turn. ### User-delegated REST APIs In Slack, if you want to use Slack REST APIs that require user-delegated scopes, you need to implement an OAuth 2.0 installation flow in your application to obtain and store Slack user tokens, even if the app was already installed by another user. In Teams, you can leverage Teams SSO to obtain user Entra tokens for calling Graph REST APIs. The Teams SDK integrates with Teams SSO and Azure Bot Token Service to handle token acquisition, storage, and refresh automatically for you. First, follow the instructions in the [Teams SSO guide](../../../../docs/main/teams/user-authentication/sso-setup.mdx). Then, configure the authentication in your code. ### User authentication for external services In Slack, you can access external services by implementing an account binding flow using OAuth 2.0 as documented [here](https://docs.slack.dev/authentication/binding-accounts-across-services). In Teams, you can access external services by implementing an OAuth 2.0 flow, with the Azure Bot Token Service handling token acquisition, storage, and refresh for you. First, setup your OAuth 2.0 connection settings in the [Azure Portal](https://portal.azure.com/) for your Azure Bot resource. ![Screenshot showing Azure Bot custom OAuth connection settings.](/screenshots/abs-custom-oauth-connection.png) Then, add the authentication code to your application to get the relevant user token and call your external service. ================================================ FILE: teams.md/src/pages/templates/migrations/v1.mdx ================================================ --- sidebar_position: 2 title: Migrating from Teams SDK v1 languages: ['typescript', 'python'] summary: Migration guide from Teams SDK v1 to v2 highlighting the key changes and upgrade steps. --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Migrating from Teams SDK v1 Welcome, fellow agent developer! You've made it through a full major release of Teams SDK, and now you want to take the plunge into v2. In this guide, we'll walk you through everything you need to know, from migrating core features like message handlers and auth, to optional AI features like `ActionPlanner`. ## Installing Teams SDK ## Migrate Application class First, migrate your `Application` class from v1 to the new `App` class. ## Migrate activity handlers Both v1 and v2 are built atop incoming `Activity` requests, which trigger handlers in your code when specific type of activities are received. The syntax for how you register different types of `Activity` handlers differs between the v1 and v2 versions of our SDK. ### Message handlers ### Task modules Learn more in the [Dialogs guide](../in-depth-guides/dialogs). ## Adaptive cards In Teams SDK v2, cards have much more rich type validation than existed in v1. However, assuming your cards were valid, it should be easy to migrate to v2. Learn more in the [Adaptive Cards guide](../in-depth-guides/adaptive-cards). ## Authentication Most agents feature authentication for user identification, interacting with APIs, etc. Whether your Teams SDK app used Entra SSO or custom OAuth, porting to v2 should be simple. ## AI ### Feedback If you supported feedback for AI generated messages, migrating is simple. You can learn more about feedback in Teams SDK in the [Feedback guide](../in-depth-guides/feedback). ================================================ FILE: teams.md/src/pages/templates/migrations/v2-previews.mdx ================================================ --- sidebar_position: 3 sidebar_label: From V2 Previews title: From V2 Previews languages: ['typescript'] --- # From V2 Previews ================================================ FILE: teams.md/src/pages/typescript.tsx ================================================ import { Redirect } from '@docusaurus/router'; import useBaseUrl from '@docusaurus/useBaseUrl'; export default function Typescript() { const baseUrl = useBaseUrl('/'); return ; } ================================================ FILE: teams.md/src/scripts/scaffold.js ================================================ #!/usr/bin/env node const fs = require('fs'); const path = require('path'); function ensureDirSync(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function createFileIfNotExists(filePath, content = '') { if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, content); } } function scaffold(userInput) { // If path starts with src/ but is not a valid base, fail if (/^src\//.test(userInput) && !/^src\/pages\/templates\//.test(userInput) && !/^src\/components\/include\//.test(userInput)) { console.error('Error: Path is outside of allowed base directories.'); process.exit(1); } // Normalize input let relPath = userInput.replace(/^src\/(pages\/templates|components\/include)\/?/, '').replace(/^[/.]+/, ''); let isTemplates = false; let isInclude = false; // Detect if user input is for templates or include if (/^(src\/)?pages\/templates\//.test(userInput) || userInput.startsWith('templates/')) { isTemplates = true; } else if (/^(src\/)?components\/include\//.test(userInput) || userInput.startsWith('include/')) { isInclude = true; } // If user gave a full path, strip to relative if (isTemplates) { relPath = userInput.replace(/^(src\/)?pages\/templates\/?/, ''); // Fail if userInput is not under templatesBase const templatesBase = path.resolve(__dirname, '../pages/templates'); const absTarget = path.resolve(templatesBase, relPath); if (!absTarget.startsWith(templatesBase)) { console.error('Error: Path is outside of templates base directory.'); process.exit(1); } } else if (isInclude) { relPath = userInput.replace(/^(src\/)?components\/include\/?/, ''); // Fail if userInput is not under includeBase const includeBase = path.resolve(__dirname, '../components/include'); const absTarget = path.resolve(includeBase, relPath); if (!absTarget.startsWith(includeBase)) { console.error('Error: Path is outside of include base directory.'); process.exit(1); } } // Helper to walk and create files in every directory function walkAndCreate(base, relPath, filesFn) { const parts = relPath.split('/').filter(Boolean); let curr = base; for (const part of parts) { curr = path.join(curr, part); ensureDirSync(curr); filesFn(curr); } return curr; } // If ambiguous, create both if (!isTemplates && !isInclude) { const templatesBase = path.join(__dirname, '../pages/templates'); const includeBase = path.join(__dirname, '../components/include'); const templatesTarget = walkAndCreate(templatesBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, '_category.json'), '{\n "label": "New Category"\n}\n'); createFileIfNotExists(path.join(dir, 'README.mdx'), '# New Template\n'); }); const includeTarget = walkAndCreate(includeBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, 'typescript.incl.md'), '# Typescript Include\n'); }); return {templatesTarget, includeTarget}; } if (isTemplates) { const templatesBase = path.join(__dirname, '../pages/templates'); const templatesTarget = walkAndCreate(templatesBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, '_category.json'), '{\n "label": "New Category"\n}\n'); createFileIfNotExists(path.join(dir, 'README.mdx'), '# New Template\n'); }); return {templatesTarget}; } if (isInclude) { const includeBase = path.join(__dirname, '../components/include'); const includeTarget = walkAndCreate(includeBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, 'typescript.incl.md'), '# Typescript Include\n'); }); return {includeTarget}; } } const userPath = process.argv[2]; function isValidPath(p) { if (!p || typeof p !== 'string') return false; if (p.startsWith('/') || p.includes('..') || p.trim() === '') return false; return true; } if (!isValidPath(userPath)) { console.error('Error: Invalid path. Use a relative path like "templates/new/two" or "include/new/two". Do not start with a slash or use "..".'); process.exit(1); } const created = scaffold(userPath); if (created) { if (created.templatesTarget && created.includeTarget) { console.log(`Scaffolded:\n Template: ${created.templatesTarget}\n Include: ${created.includeTarget}`); } else if (created.templatesTarget) { console.log(`Scaffolded Template: ${created.templatesTarget}`); } else if (created.includeTarget) { console.log(`Scaffolded Include: ${created.includeTarget}`); } } ================================================ FILE: teams.md/src/theme/DocSidebarItems/index.tsx ================================================ import React, { useMemo, useRef } from 'react'; import DocSidebarItemsOriginal from '@theme-original/DocSidebarItems'; import type { Props } from '@theme/DocSidebarItems'; import { useLocation } from '@docusaurus/router'; import useBaseUrl from '@docusaurus/useBaseUrl'; import { useLanguagePreference } from '../../hooks/useLanguagePreference'; import { getLanguageFromPathStrict } from '../../utils/languageUtils'; function buildItemsSignature(items: Props['items']): string { if (!Array.isArray(items)) return ''; return items .map((item) => { if ('href' in item && typeof item.href === 'string') return `L:${item.href}`; if ('items' in item && Array.isArray(item.items)) { const children = item.items; // String of all child hrefs in a category const childSig = children .map((ch) => ('href' in ch && typeof ch.href === 'string' ? ch.href : '#')) .join(','); return `C:[${childSig}]`; } return 'OTHER'; }) .join('|'); } export default function DocSidebarItems(props: Props): React.JSX.Element { const location = useLocation(); const baseUrl = useBaseUrl('/'); const { language: preferredLanguage } = useLanguagePreference(); const sidebarRef = useRef>(new Map()); // Use language from URL if present, otherwise fall back to user preference const urlLanguage = getLanguageFromPathStrict(location.pathname, baseUrl); const currentLanguage = urlLanguage || preferredLanguage; // Filter to show /docs/main content + current language content only const filteredItems = useMemo(() => { if (sidebarRef.current.has(currentLanguage)) { return sidebarRef.current.get(currentLanguage)!; } const items = Array.isArray(props.items) ? props.items : []; // Cache language detection per href const langCache = new Map(); const getCachedLanguage = (href: string) => { if (!langCache.has(href)) { langCache.set(href, getLanguageFromPathStrict(href, baseUrl)); } return langCache.get(href); }; const result = items.filter((item) => { // Sidebar item properties const itemHref = 'href' in item ? item.href : undefined; // For category items, check if they have children that would indicate language content if ('items' in item && item.items && Array.isArray(item.items)) { // Check if any child item has a language-specific href let hasLanguageContent = false; let firstLanguageMatch: string | null = null; for (const child of item.items) { const h = 'href' in child && typeof child.href === 'string' ? child.href : undefined; if (!h) { continue; } const l = getCachedLanguage(h); if (l !== null) { hasLanguageContent = true; if (firstLanguageMatch === null) { firstLanguageMatch = l; break; } } } // If this category contains language-specific content, check if it matches current language if (hasLanguageContent) { return firstLanguageMatch === currentLanguage; } } // Check if this item corresponds to a language directory by examining its href if (itemHref && typeof itemHref === 'string') { // Use explicit language detection that returns null if no language found const itemLanguage = getCachedLanguage(itemHref); // If this item links to a language directory, only show if it matches current language if (itemLanguage !== null) { return itemLanguage === currentLanguage; } } // For anything else (individual docs, categories without language context), keep them all return true; }); sidebarRef.current.set(currentLanguage, result); return result; }, [buildItemsSignature(props.items), baseUrl, currentLanguage]); // Pass filtered items to original component return ; } ================================================ FILE: teams.md/src/theme/Navbar/Content/index.tsx ================================================ import React from 'react'; import { useThemeConfig } from '@docusaurus/theme-common'; import { splitNavbarItems, useNavbarMobileSidebar } from '@docusaurus/theme-common/internal'; import NavbarItem from '@theme/NavbarItem'; import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; import NavbarLogo from '@theme/Navbar/Logo'; import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; import NavbarSearch from '@theme/Navbar/Search'; import SearchBar from '@theme/SearchBar'; import LanguageDropdown from '../../../components/LanguageDropdown'; function useNavbarItems() { return useThemeConfig().navbar.items; } function NavbarItems({ items }) { return ( <> {items.map((item, i) => ( ))} ); } function NavbarContentLayout({ left, right }) { return (
{left}
{right}
); } export default function NavbarContent(): React.JSX.Element { const mobileSidebar = useNavbarMobileSidebar(); const items = useNavbarItems(); const [leftItems, rightItems] = splitNavbarItems(items); return ( {!mobileSidebar.disabled && } } right={ <> } /> ); } ================================================ FILE: teams.md/src/theme/PaginatorNavLink/index.tsx ================================================ import React from 'react'; import PaginatorNavLinkOriginal from '@theme-original/PaginatorNavLink'; import type { Props } from '@theme/PaginatorNavLink'; import useBaseUrl from '@docusaurus/useBaseUrl'; import { useLanguagePreference } from '../../hooks/useLanguagePreference'; import { getLanguageFromPathStrict, replaceLanguageInPath } from '../../utils/languageUtils'; export default function PaginatorNavLink(props: Props): React.JSX.Element { const baseUrl = useBaseUrl('/'); const { language: preferredLanguage } = useLanguagePreference(); // If no permalink, use original behavior if (!props.permalink) { return ; } // Check if the permalink contains a language path const languageInPermalink = getLanguageFromPathStrict(props.permalink, baseUrl); // If the link contains a language path, update it to match current preferred language if (languageInPermalink !== null && languageInPermalink !== preferredLanguage) { const correctedPermalink = replaceLanguageInPath(props.permalink, baseUrl, preferredLanguage); return ; } // Otherwise use original return ; } ================================================ FILE: teams.md/src/theme/Root.tsx ================================================ import React, { ReactNode } from 'react'; import { LanguageProvider } from '../hooks/useLanguagePreference'; // Provide language context to the entire docusaurus app export default function Root({ children }: { children: ReactNode }): React.JSX.Element { return {children}; } ================================================ FILE: teams.md/src/utils/languageUtils.ts ================================================ import { LANGUAGES, type Language } from '../constants/languages'; /** * Creates the regex pattern for matching language paths * @param baseUrl - The base URL * @returns RegExp for matching language paths */ function createLanguagePattern(baseUrl: string): RegExp { return new RegExp(`^${baseUrl}(${LANGUAGES.join('|')})(/|$)`); } /** * Gets the current language from a URL pathname * @param pathname - The current pathname (e.g. from useLocation) * @param baseUrl - The base URL (e.g. from useBaseUrl) * @returns The detected language or 'typescript' as fallback */ export function getLanguageFromPath(pathname: string, baseUrl: string): Language { const languagePattern = createLanguagePattern(baseUrl); const match = pathname.match(languagePattern); return match ? (match[1] as Language) : 'typescript'; } /** * Detects if a URL contains a language and returns it, or null if none found * @param pathname - The pathname to check * @param baseUrl - The base URL * @returns The detected language or null */ export function getLanguageFromPathStrict(pathname: string, baseUrl: string): Language | null { const languagePattern = createLanguagePattern(baseUrl); const match = pathname.match(languagePattern); return match ? (match[1] as Language) : null; } /** * Replaces the language in a URL path with a new language * @param pathname - The pathname to modify * @param baseUrl - The base URL * @param newLanguage - The new language to replace with * @returns The modified pathname, or original if no language was found */ export function replaceLanguageInPath( pathname: string, baseUrl: string, newLanguage: Language ): string { const languagePattern = createLanguagePattern(baseUrl); const match = pathname.match(languagePattern); if (match) { return pathname.replace(languagePattern, `${baseUrl}${newLanguage}/`); } return pathname; } /** * Converts a URL path to the manifest path format used for language availability checking * @param pathname - The full URL pathname * @param baseUrl - The base URL * @returns The manifest path format (removes base and language, handles root) */ export function getManifestPathFromUrl(pathname: string, baseUrl: string): string { // Remove base url and language: const urlPath = pathname.replace(baseUrl, '').replace(/^[^/]+\//, ''); // Remove trailing slash; use '/' for root return urlPath.replace(/\/$/, '') || '/'; } ================================================ FILE: teams.md/src/utils/normalizePath.ts ================================================ /** * For cross-platform compatibility, normalize paths to use forward slash */ export default function normalizePath(path: string) { return path.replace(/\\/g, '/'); } ================================================ FILE: teams.md/src/utils/pageAvailability.ts ================================================ import { type Language, type LanguageAvailabilityMap } from '../constants/languages'; // Cache for the missing pages data let missingPagesCache: LanguageAvailabilityMap | null = null; /** * Check if a page exists for a specific language by consulting the missing pages manifest * @param pagePath - The manifest path * @param language - The target language to check * @returns Promise - true: page available; else false */ export async function isPageAvailableForLanguage(pagePath: string, language: Language): Promise { if (!missingPagesCache) { try { const response = await fetch('/teams-sdk/missing-pages.json'); if (response.ok) { missingPagesCache = await response.json(); } else { missingPagesCache = {}; } } catch { missingPagesCache = {}; } } const unavailableLanguages = missingPagesCache[pagePath]; // If page is found, it's unavailable for the specified language(s) return !unavailableLanguages || !unavailableLanguages.includes(language); } ================================================ FILE: teams.md/src/utils/readFileUtf8Normalized.ts ================================================ import * as fs from 'fs'; /** * Read a UTF-8 file and normalize CRLF (\r\n) to LF (\n). * This ensures downstream string operations don't see stray \r characters. */ export default function readFileUtf8Normalized(filePath: string): string { return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n'); } ================================================ FILE: teams.md/static/.nojekyll ================================================ ================================================ FILE: teams.md/static/llms_docs/llms.txt ================================================ # Teams AI Library Documentation This website contains documentation for interacting with Teams AI Library, an SDK built to simplify building agents and applications that will integrate with Microsoft Teams. IMPORTANT: This SDK is NOT built using BotFramework (which was an older iteration). Language Specific URLs: - [Typescript Documentation](https://microsoft.github.io/teams-ai/llms_docs/llms_typescript.txt) - [Dotnet (C#) Documentation](https://microsoft.github.io/teams-ai/llms_docs/llms_typescript.txt) - [Python Documentation](https://microsoft.github.io/teams-ai/llms_docs/llms_python.txt) ================================================ FILE: teams.md/static/missing-pages.json ================================================ { "essentials/on-activity/activity-ref": [ "csharp", "python" ], "in-depth-guides/ai/a2a": [ "csharp" ], "in-depth-guides/ai/a2a/a2a-client": [ "csharp" ], "in-depth-guides/ai/a2a/a2a-server": [ "csharp" ], "in-depth-guides/ai/mcp/mcp-server": [ "csharp" ], "in-depth-guides/server/http-server": [ "csharp" ], "in-depth-guides/tabs": [ "python" ], "in-depth-guides/tabs/app-options": [ "csharp", "python" ], "in-depth-guides/tabs/functions": [ "python" ], "in-depth-guides/tabs/functions/function-calling": [ "csharp", "python" ], "in-depth-guides/tabs/getting-started": [ "csharp", "python" ], "in-depth-guides/tabs/graph": [ "csharp", "python" ], "in-depth-guides/tabs/using-the-app": [ "csharp", "python" ], "migrations": [ "csharp" ], "migrations/slack-bolt": [ "csharp", "python" ], "migrations/v1": [ "csharp" ], "migrations/v2-previews": [ "csharp", "python" ] } ================================================ FILE: teams.md/static/scripts/clarity.js ================================================ (function(c,l,a,r,i,t,y){ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); })(window, document, "clarity", "script", "rdeztc00ot"); ================================================ FILE: teams.md/tsconfig.json ================================================ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { "baseUrl": "." }, "exclude": [".docusaurus", "build"] } ================================================ FILE: turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**"] }, "lint": { "dependsOn": ["^lint"] }, "check-types": { "dependsOn": ["^check-types"] }, "dev": { "cache": false, "persistent": true } } }