Skip to content
BotServBotServ
Microsoft TeamsBotAI BotOllamaBot FrameworkEnterpriseAzure

Microsoft Teams Bot with Local AI

Build a Teams bot with Ollama: Bot Framework, Azure Bot Service, Adaptive Cards, and enterprise AI.

S

schutzgeist

7 min read
Microsoft Teams Bot with Local AI

Microsoft Teams Bot with Local AI

What This Article Covers

  • Building a Teams bot with the Bot Framework and Ollama.
  • Azure Bot Service versus local alternatives, and what must stay in Azure.
  • Adaptive Cards, mentions, Teams channels, and proactive messaging.
  • Extensions: RAG on SharePoint/M365 data, meeting summaries.
  • Why Teams bots are the enterprise heavyweight of chat platforms.

Introduction

Microsoft Teams is the standard chat platform in enterprises running Microsoft 365, and it’s the one where you can’t just register your bot anywhere. Teams bots require an Azure Bot Resource as a broker: Teams never talks directly to your server, always through Azure Bot Service.

This adds setup overhead: app registration, Azure Bot, Teams manifest, channel configuration. But you gain enterprise features: Adaptive Cards, Graph API integration, SSO with Entra ID. The AI itself can run locally; only the message transport goes through Azure.

Typical Use Cases

  • Internal helpdesk bot: Answer IT questions from your company wiki using RAG.
  • HR assistant: Vacation policies, benefits, forms, employees ask the bot.
  • DevOps assistant: Build status, deployment questions, incident support.
  • Meeting assistant: Summarize transcripts (combined with Graph API).
  • Compliance-aware AI chat: AI stays local, only the channel is Microsoft.

Prerequisites

  • Microsoft 365 tenant with Teams
  • Azure subscription (Bot Service is free on the Free tier)
  • Publicly reachable HTTPS endpoint for your bot (Azure calls your server), or a tunnel like devtunnel/cloudflared for development
  • Python or C# (Bot Framework SDK supports both)
  • Ollama with a model

Step 1: Create an Azure Bot Resource

  1. Azure Portal → create “Azure Bot” → type “Single Tenant” or “Multi Tenant”
  2. Microsoft App ID is generated, create a Client Secret
  3. Channels → enable “Microsoft Teams”
  4. Set messaging endpoint: https://bots.your-domain.com/api/messages

.env:

APP_ID=your-app-id
APP_PASSWORD=your-client-secret
APP_TYPE=MultiTenant
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b

Step 2: Build the Bot with Bot Framework (Python)

pip install botbuilder-core botbuilder-schema aiohttp ollama python-dotenv

bot.py:

import os
import ollama
from aiohttp import web
from dotenv import load_dotenv
from botbuilder.core import (
    BotFrameworkAdapterSettings, BotFrameworkAdapter, TurnContext, ActivityHandler
)
from botbuilder.schema import Activity, ActivityTypes

load_dotenv()

SETTINGS = BotFrameworkAdapterSettings(
    os.environ["APP_ID"], os.environ["APP_PASSWORD"]
)
ADAPTER = BotFrameworkAdapter(SETTINGS)
ollama_client = ollama.Client(host=os.environ["OLLAMA_URL"])
MODEL = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")

SYSTEM = ("You are the company's internal AI assistant. "
          "Keep answers brief, helpful, and in English.")

# Conversation context per Teams conversation
conversations = {}


class KIBot(ActivityHandler):
    async def on_message_activity(self, turn_context: TurnContext):
        conv_id = turn_context.activity.conversation.id
        text = turn_context.activity.text or ""

        # Teams supplies <at>Bot-Name</at> in mentions, remove them
        text = text.replace("<at>", "").replace("</at>", "")
        for entity in turn_context.activity.entities or []:
            if entity.type == "mention":
                text = text.replace(entity.text, "")
        text = text.strip()

        if not text:
            return

        history = conversations.setdefault(conv_id, [])
        history.append({"role": "user", "content": text})
        history = history[-10:]

        # Typing indicator
        await turn_context.send_activity(Activity(type=ActivityTypes.typing))

        try:
            resp = ollama_client.chat(model=MODEL, messages=[
                {"role": "system", "content": SYSTEM}] + history)
            answer = resp["message"]["content"]
            history.append({"role": "assistant", "content": answer})
            conversations[conv_id] = history
            await turn_context.send_activity(answer[:28000])
        except Exception as e:
            await turn_context.send_activity("Error: AI backend unreachable.")


BOT = KIBot()

async def messages(request):
    body = await request.json()
    auth_header = request.headers.get("Authorization", "")
    activity = Activity().deserialize(body)
    response = await ADAPTER.process_activity(
        activity, auth_header, BOT.on_turn
    )
    return web.Response(status=200)

app = web.Application()
app.router.add_post("/api/messages", messages)
web.run_app(app, host="0.0.0.0", port=3978)

Step 3: Teams Manifest and App Package

Teams needs an app package (ZIP containing manifest.json and icons):

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
  "manifestVersion": "1.16",
  "version": "1.0.0",
  "id": "your-app-id-from-azure-bot",
  "developer": {
    "name": "Your Company",
    "websiteUrl": "https://your-domain.com",
    "privacyUrl": "https://your-domain.com/privacy",
    "termsOfUseUrl": "https://your-domain.com/terms"
  },
  "name": {"short": "AI Assistant"},
  "description": {"short": "Internal AI bot", "full": "AI assistant powered by a local model"},
  "icons": {"color": "color.png", "outline": "outline.png"},
  "accentColor": "#FFFFFF",
  "bots": [{
    "botId": "your-app-id-from-azure-bot",
    "scopes": ["personal", "team", "groupChat"],
    "supportsFiles": false,
    "isNotificationOnly": false
  }],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": []
}

Package the ZIP, then go to Teams → Apps → “Manage your apps” → “Upload a custom app”. In corporate tenants, the admin must enable sideloading or approve the app in the Admin Center.

Extensions

Adaptive Cards (Buttons, Forms)

card = {
    "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.4",
    "body": [{"type": "TextBlock", "text": "Was this answer helpful?"}],
    "actions": [
        {"type": "Action.Submit", "title": "👍", "data": {"vote": "up"}},
        {"type": "Action.Submit", "title": "👎", "data": {"vote": "down"}},
        {"type": "Action.Submit", "title": "Escalate to team",
         "data": {"escalate": True}},
    ]
}
await turn_context.send_activity(Activity(
    type=ActivityTypes.message,
    attachments=[{"contentType": "application/vnd.microsoft.card.adaptive",
                  "content": card}]
))

Button clicks arrive as on_message_activity with value instead of text; handle the logic there.

RAG on SharePoint / Internal Documentation

Export SharePoint documents via Graph API, chunk them, index them in Qdrant, and have your bot answer from company knowledge. Alternatively, use local documents as described in Local RAG.

Proactive Messaging

The bot can message users unprompted (for example, “Your ticket is resolved”). You need to save the conversationReference from the first interaction:

async def on_members_added_activity(self, members_added, turn_context):
    # Save conversation reference for later
    ref = TurnContext.get_conversation_reference(turn_context.activity)
    save_ref(turn_context.activity.from_property.id, ref)

# later, proactively:
await ADAPTER.continue_conversation(ref, callback)

Entra ID / SSO

Since the bot runs in the Microsoft ecosystem anyway, you can get user identity through Entra ID. The bot then knows who’s asking and can personalize responses or verify permissions.

Deployment

Teams calls your endpoint, so the bot server needs public HTTPS:

services:
  teams-bot:
    build: ./bot
    restart: always
    env_file: .env
    ports: ["127.0.0.1:3978:3978"]
    # Reverse Proxy (Caddy/Nginx) handles TLS + forwarding

Development tip: use devtunnel (Microsoft) or cloudflared as a temporary tunnel without configuring DNS or a proxy.

Security and Compliance

  • Hybrid data protection: Messages travel Teams → Azure Bot Service → your server. AI processing stays local, but Microsoft sees the traffic.
  • App Secret: store the Client Secret in Azure Key Vault or .env, never in the manifest.
  • Tenant validation: set APP_TYPE correctly (SingleTenant for your company only).
  • Permissions: the bot only sees conversations it’s been invited to, which is good.
  • GDPR: Teams can be used GDPR-compliant (EU Data Boundary); the Azure Bot Service passes through. For the highest confidentiality, use a pure on-premise system like Mattermost.

Common Pitfalls

  • 401 Unauthorized: app ID or secret is wrong, or APP_TYPE doesn’t match the tenant type.
  • Bot doesn’t reply in channels: the bot must be invited to the channel or team (@mention activates it for the team).
  • Mention markup: remove <at> tags from text, otherwise they end up in the prompt.
  • App won’t install: sideloading is disabled in the tenant. An admin needs to enable it.
  • Endpoint must be HTTPS: Azure only accepts TLS with a valid certificate, not self-signed.

Further Reading

Key Takeaways:

  • Teams bots require Azure Bot Service as an intermediary; there’s no direct path.
  • Setup involves Azure Bot + App Registration + Teams manifest, more involved than Slack or Telegram.
  • Message transport goes through Azure, but AI processing can stay local (hybrid).
  • Adaptive Cards and Graph API are the strong enterprise features.
  • For M365 companies, it’s the only sensible choice; for everyone else, too complex.

FAQ

Does the bot really have to run through Azure?

Your bot code runs on your server, but message transport must go through the Azure Bot Service. Without an Azure resource, there’s no Teams bot. The Azure Bot resource itself is free in the free tier.

How much does a Teams bot cost?

Azure Bot Service is free in the free tier (10,000 messages per month on premium channels). Teams itself requires M365 licenses. Your local AI only needs hardware. No bot usage fees like GPT APIs.

Python or C# for the bot?

Both have official SDKs. C# is the reference implementation with the most examples; Python (botbuilder-core) is simpler for AI integration (Ollama, LangChain, etc. are Python-first).

How do I get the app into the company tenant?

Upload as a ZIP via “Upload a custom app” if the tenant allows sideloading. Otherwise, an admin uploads it in the Teams Admin Center or distributes it via app policy. For company-wide rollout, have it published in the Admin Center.

Teams bot or Slack bot?

If the company uses M365, go with Teams; users are already there and SSO is free. Slack is easier to develop (no Azure requirement). For complete data ownership, use Mattermost.

Can Microsoft see the bot messages?

Transport goes through Teams/Azure, so technically yes, as a pass-through. AI processing and documents stay local. For highly confidential content, a pure on-premise system (Mattermost or Matrix) is better.

Can the bot message users proactively?

Yes, proactive messaging is possible, but it requires a saved conversationReference from the first contact. Useful for alerts and notifications.

Sources and Further Reading

Back to Blog
Share:

Related Posts