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
- Azure Portal → create “Azure Bot” → type “Single Tenant” or “Multi Tenant”
- Microsoft App ID is generated, create a Client Secret
- Channels → enable “Microsoft Teams”
- 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_TYPEcorrectly (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_TYPEdoesn’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
- IRC-Coding.de: in-depth programming tutorials on Bot Framework, Adaptive Cards, and Azure integration.
- Slack-Bot: the simpler enterprise alternative.
- Mattermost-Bot: completely self-hosted alternative.
- Platform Comparison: all platforms compared.
- Ollama API: model server interface.
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?
How much does a Teams bot cost?
Python or C# for the bot?
How do I get the app into the company tenant?
Teams bot or Slack bot?
Can Microsoft see the bot messages?
Can the bot message users proactively?
Sources and Further Reading
- Bot Framework SDK: Python SDK.
- Teams App Manifest: manifest reference.
- Azure Bot Service: bot service.
- IRC-Coding.de: programming tutorials.


