Microsoft Teams Apps Skill
Load with: base.md
Purpose: Build AI-powered agents and apps for Microsoft Teams. Create conversational bots, message extensions, and intelligent assistants that integrate with LLMs like OpenAI and Claude.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ TEAMS APP TYPES │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 1. AI AGENTS (Bots) │
│ Conversational apps powered by LLMs │
│ Handle messages, commands, and actions │
│ │
│ 2. MESSAGE EXTENSIONS │
│ Search external systems, insert cards into messages │
│ Action commands with modal dialogs │
│ │
│ 3. TABS │
│ Embedded web applications inside Teams │
│ Personal, channel, or meeting tabs │
│ │
│ 4. WEBHOOKS & CONNECTORS │
│ Incoming: Post messages to channels │
│ Outgoing: Respond to @mentions │
├─────────────────────────────────────────────────────────────────┤
│ SDK LANDSCAPE (2025) │
│ ───────────────────────────────────────────────────────────── │
│ Teams SDK v2: Primary SDK for Teams-only apps │
│ M365 Agents SDK: Multi-channel (Teams, Outlook, Copilot) │
│ Teams Toolkit: VS Code extension for development │
└─────────────────────────────────────────────────────────────────┘
Quick Start
Install Teams CLI
npm install -g @microsoft/teams.cli
Create New Project
# TypeScript (Recommended)
npx @microsoft/teams.cli new typescript my-agent --template echo
# Python
npx @microsoft/teams.cli new python my-agent --template echo
# C#
npx @microsoft/teams.cli new csharp my-agent --template echo
Project Structure
my-agent/
├── src/
│ ├── index.ts # Entry point
│ ├── app.ts # App configuration
│ └── handlers/
│ ├── message.ts # Message handlers
│ └── commands.ts # Command handlers
├── appPackage/
│ ├── manifest.json # App manifest
│ ├── color.png # App icon (192x192)
│ └── outline.png # Outline icon (32x32)
├── .env # Environment variables
├── teamsapp.yml # Teams Toolkit config
└── package.json
App Manifest
Basic Manifest Structure
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
"manifestVersion": "1.17",
"version": "1.0.0",
"id": "{{APP_ID}}",
"developer": {
"name": "Your Company",
"websiteUrl": "https://yourcompany.com",
"privacyUrl": "https://yourcompany.com/privacy",
"termsOfUseUrl": "https://yourcompany.com/terms"
},
"name": {
"short": "AI Assistant",
"full": "AI Assistant for Teams"
},
"description": {
"short": "Your AI-powered assistant",
"full": "An intelligent assistant that helps you with tasks using AI."
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"accentColor": "#5558AF",
"bots": [
{
"botId": "{{BOT_ID}}",
"scopes": ["personal", "team", "groupChat"],
"supportsFiles": false,
"isNotificationOnly": false,
"commandLists": [
{
"scopes": ["personal", "team", "groupChat"],
"commands": [
{
"title": "help",
"description": "Show available commands"
},
{
"title": "ask",
"description": "Ask the AI a question"
}
]
}
]
}
],
"permissions": ["identity", "messageTeamMembers"],
"validDomains": ["*.azurewebsites.net"]
}
Manifest with Message Extensions
{
"composeExtensions": [
{
"botId": "{{BOT_ID}}",
"commands": [
{
"id": "searchQuery",
"type": "query",
"title": "Search",
"description": "Search for information",
"initialRun": true,
"parameters": [
{
"name": "query",
"title": "Search query",
"description": "Enter your search terms",
"inputType": "text"
}
]
},
{
"id": "createTask",
"type": "action",
"title": "Create Task",
"description": "Create a new task",
"fetchTask": true,
"context": ["compose", "commandBox", "message"]
}
]
}
]
}
AI Agent Development
Basic Bot with Teams SDK v2
// src/app.ts
import { App, HttpPlugin, DevtoolsPlugin } from '@microsoft/teams.ai';
import { OpenAIModel, ActionPlanner, PromptManager } from '@microsoft/teams.ai';
// Configure the AI model
const model = new OpenAIModel({
azureApiKey: process.env.AZURE_OPENAI_API_KEY!,
azureDefaultDeployment: process.env.AZURE_OPENAI_DEPLOYMENT!,
azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!,
// Or use OpenAI directly:
// apiKey: process.env.OPENAI_API_KEY!,
// defaultModel: 'gpt-4'
});
// Configure prompts
const prompts = new PromptManager({
promptsFolder: './src/prompts'
});
// Create action planner
const planner = new ActionPlanner({
model,
prompts,
defaultPrompt: 'chat'
});
// Create the app
const app = new App({
plugins: [
new HttpPlugin(),
new DevtoolsPlugin()
],
ai: {
planner
}
});
// Handle messages
app.on('message', async (context, state) => {
// AI automatically handles the conversation
// The planner uses the 'chat' prompt to generate responses
});
// Handle specific commands
app.message('/help', async (context, state) => {
await context.sendActivity({
type: 'message',
text: 'Available commands:\n- /help - Show this message\n- /ask [question] - Ask me anything'
});
});
// Start the app
app.start();
Prompt Configuration
# src/prompts/chat/config.json
{
"schema": 1.1,
"description": "AI Assistant for Teams",
"type": "completion",
"completion": {
"model": "gpt-4",
"max_tokens": 1000,
"temperature": 0.7,
"top_p": 1
}
}
# src/prompts/chat/skprompt.txt
You are an AI assistant for Microsoft Teams. You help users with their questions and tasks.
Current conversation:
{{$history}}
User: {{$input}}
Assistant:
Integrating Claude/Anthropic
Claude-Powered Teams Bot
// src/claude-bot.ts
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!
});
const app = new App({
plugins: [new HttpPlugin()]
});
// Conversation history store
const conversations = new Map<string, Anthropic.MessageParam[]>();
app.on('message', async (context, state) => {
const userId = context.activity.from.id;
const userMessage = context.activity.text;
// Get or initialize conversation history
if (!conversations.has(userId)) {
conversations.set(userId, []);
}
const history = conversations.get(userId)!;
// Add user message to history
history.push({ role: 'user', content: userMessage });
// Show typing indicator
await context.sendActivity({ type: 'typing' });
try {
// Call Claude API
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: `You are an AI assistant integrated into Microsoft Teams.
Help users with their questions and tasks.
Be concise and helpful. Use markdown formatting when appropriate.
Current user: ${context.activity.from.name}`,
messages: history
});
const assistantMessage = response.content[0].type === 'text'
? response.content[0].text
: '';
// Add assistant response to history
history.push({ role: 'assistant', content: assistantMessage });
// Keep history manageable (last 20 messages)
if (history.length > 20) {
history.splice(0, history.length - 20);
}
// Send response
await context.sendActivity({
type: 'message',
text: assistantMessage
});
} catch (error) {
console.error('Claude API error:', error);
await context.sendActivity({
type: 'message',
text: 'Sorry, I encountered an error processing your request.'
});
}
});
// Clear conversation command
app.message('/clear', async (context, state) => {
const userId = context.activity.from.id;
conversations.delete(userId);
await context.sendActivity('Conversation cleared. Starting fresh!');
});
app.start();
Claude with Tools/Function Calling
// src/claude-agent.ts
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
// Define tools the agent can use
const tools: Anthropic.Tool[] = [
{
name: 'search_knowledge_base',
description: 'Search the company knowledge base for information',
input_schema: {
type: 'object' as const,
properties: {
query: {
type: 'string',
description: 'The search query'
}
},
required: ['query']
}
},
{
name: 'create_task',
description: 'Create a new task in the task management system',
input_schema: {
type: 'object' as const,
properties: {
title: { type: 'string', description: 'Task title' },
description: { type: 'string', description: 'Task description' },
assignee: { type: 'string', description: 'Person to assign the task to' },
due_date: { type: 'string', description: 'Due date in YYYY-MM-DD format' }
},
required: ['title']
}
},
{
name: 'get_calendar',
description: 'Get calendar events for a user',
input_schema: {
type: 'object' as const,
properties: {
user: { type: 'string', description: 'User email or name' },
days: { type: 'number', description: 'Number of days to look ahead' }
},
required: ['user']
}
}
];
// Tool implementations
async function executeTools(toolName: string, toolInput: any): Promise<string> {
switch (toolName) {
case 'search_knowledge_base':
// Implement your search logic
return `Found 3 results for "${toolInput.query}":\n1. Document A\n2. Document B\n3. Document C`;
case 'create_task':
// Implement task creation (e.g., call Microsoft Graph API)
return `Task created: "${toolInput.title}"`;
case 'get_calendar':
// Implement calendar lookup
return `Calendar for ${toolInput.user}: 2 meetings today`;
default:
return 'Unknown tool';
}
}
// Agent loop with tool use
async function runAgent(userMessage: string): Promise<string> {
let messages: Anthropic.MessageParam[] = [
{ role: 'user', content: userMessage }
];
while (true) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: 'You are a helpful Teams assistant. Use tools when needed to help users.',
tools,
messages
});
// Check if we need to use tools
if (response.stop_reason === 'tool_use') {
const toolResults: Anthropic.MessageParam[] = [];
for (const content of response.content) {
if (content.type === 'tool_use') {
const result = await executeTools(content.name, content.input);
toolResults.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: content.id,
content: result
}]
});
}
}
messages.push({ role: 'assistant', content: response.content });
messages.push(...toolResults);
continue;
}
// Return final text response
const textContent = response.content.find(c => c.type === 'text');
return textContent?.text || 'No response';
}
}
Adaptive Cards
Basic Adaptive Card
// src/cards/welcome-card.ts
import { CardFactory } from 'botbuilder';
export function createWelcomeCard(userName: string) {
return CardFactory.adaptiveCard({
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
body: [
{
type: 'TextBlock',
text: `Welcome, ${userName}!`,
size: 'Large',
weight: 'Bolder'
},
{
type: 'TextBlock',
text: 'I\'m your AI assistant. How can I help you today?',
wrap: true
},
{
type: 'ActionSet',
actions: [
{
type: 'Action.Submit',
title: 'Get Started',
data: { action: 'getStarted' }
},
{
type: 'Action.Submit',
title: 'View Help',
data: { action: 'help' }
}
]
}
]
});
}
AI Response Card with Actions
// src/cards/ai-response-card.ts
export function createAIResponseCard(
question: string,
answer: string,
sources?: string[]
) {
return {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
body: [
{
type: 'Container',
style: 'emphasis',
items: [
{
type: 'TextBlock',
text: 'Your Question',
size: 'Small',
weight: 'Bolder'
},
{
type: 'TextBlock',
text: question,
wrap: true
}
]
},
{
type: 'Container',
items: [
{
type: 'TextBlock',
text: 'AI Response',
size: 'Small',
weight: 'Bolder'
},
{
type: 'TextBlock',
text: answer,
wrap: true
}
]
},
...(sources && sources.length > 0 ? [{
type: 'Container',
items: [
{
type: 'TextBlock',
text: 'Sources',
size: 'Small',
weight: 'Bolder'
},
...sources.map(source => ({
type: 'TextBlock',
text: `• ${source}`,
size: 'Small'
}))
]
}] : [])
],
actions: [
{
type: 'Action.Submit',
title: '👍 Helpful',
data: { action: 'feedback', value: 'positive' }
},
{
type: 'Action.Submit',
title: '👎 Not Helpful',
data: { action: 'feedback', value: 'negative' }
},
{
type: 'Action.Submit',
title: 'Ask Follow-up',
data: { action: 'followUp' }
}
]
};
}
Form Card for User Input
// src/cards/task-form-card.ts
export function createTaskFormCard() {
return {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
body: [
{
type: 'TextBlock',
text: 'Create New Task',
size: 'Large',
weight: 'Bolder'
},
{
type: 'Input.Text',
id: 'taskTitle',
label: 'Task Title',
isRequired: true,
placeholder: 'Enter task title'
},
{
type: 'Input.Text',
id: 'taskDescription',
label: 'Description',
isMultiline: true,
placeholder: 'Enter task description'
},
{
type: 'Input.ChoiceSet',
id: 'priority',
label: 'Priority',
choices: [
{ title: 'High', value: 'high' },
{ title: 'Medium', value: 'medium' },
{ title: 'Low', value: 'low' }
],
value: 'medium'
},
{
type: 'Input.Date',
id: 'dueDate',
label: 'Due Date'
}
],
actions: [
{
type: 'Action.Submit',
title: 'Create Task',
data: { action: 'createTask' }
},
{
type: 'Action.Submit',
title: 'Cancel',
data: { action: 'cancel' }
}
]
};
}
Microsoft Graph Integration
Setup Graph Client
// src/graph/client.ts
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { ClientSecretCredential } from '@azure/identity';
export function createGraphClient() {
const credential = new ClientSecretCredential(
process.env.AZURE_TENANT_ID!,
process.env.AZURE_CLIENT_ID!,
process.env.AZURE_CLIENT_SECRET!
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ['https://graph.microsoft.com/.default']
});
return Client.initWithMiddleware({ authProvider });
}
Common Graph Operations
// src/graph/operations.ts
import { Client } from '@microsoft/microsoft-graph-client';
export class GraphOperations {
constructor(private client: Client) {}
// Get user profile
async getUserProfile(userId: string) {
return this.client.api(`/users/${userId}`).get();
}
// Get user's calendar events
async getCalendarEvents(userId: string, days: number = 7) {
const startDate = new Date().toISOString();
const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
return this.client
.api(`/users/${userId}/calendarView`)
.query({
startDateTime: startDate,
endDateTime: endDate
})
.select('subject,start,end,location')
.orderby('start/dateTime')
.get();
}
// Send email
async sendEmail(
fromUserId: string,
to: string,
subject: string,
body: string
) {
return this.client.api(`/users/${fromUserId}/sendMail`).post({
message: {
subject,
body: { contentType: 'HTML', content: body },
toRecipients: [{ emailAddress: { address: to } }]
}
});
}
// Create Teams meeting
async createMeeting(
userId: string,
subject: string,
startTime: string,
endTime: string,
attendees: string[]
) {
return this.client.api(`/users/${userId}/onlineMeetings`).post({
subject,
startDateTime: startTime,
endDateTime: endTime,
participants: {
attendees: attendees.map(email => ({
upn: email,
role: 'attendee'
}))
}
});
}
// Post message to channel
async postToChannel(teamId: string, channelId: string, message: string) {
return this.client
.api(`/teams/${teamId}/channels/${channelId}/messages`)
.post({
body: { content: message }
});
}
}
Authentication
SSO with Teams SDK
// src/auth.ts
import { App } from '@microsoft/teams.ai';
const app = new App({
// ... other config
});
app.on('message', async ({ userGraph, isSignedIn, send, signin }) => {
// Check if user is signed in
if (!isSignedIn) {
// Initiate sign-in flow
await signin();
return;
}
// User is signed in, access Graph API
const me = await userGraph.call({
method: 'GET',
path: '/me'
});
await send(`Hello, ${me.displayName}!`);
});
Manual OAuth Flow
// src/auth/oauth.ts
import { OAuthPrompt, OAuthPromptSettings } from 'botbuilder-dialogs';
const oauthSettings: OAuthPromptSettings = {
connectionName: process.env.OAUTH_CONNECTION_NAME!,
text: 'Please sign in to continue',
title: 'Sign In',
timeout: 300000 // 5 minutes
};
// In your dialog
async function handleAuth(context, state) {
const tokenResponse = await context.adapter.getUserToken(
context,
oauthSettings.connectionName
);
if (!tokenResponse?.token) {
// No token, show sign-in card
await context.sendActivity({
attachments: [
CardFactory.oauthCard(
oauthSettings.connectionName,
oauthSettings.title,
oauthSettings.text
)
]
});
return null;
}
return tokenResponse.token;
}
RAG (Retrieval-Augmented Generation)
Vector Search with Azure AI Search
// src/rag/azure-search.ts
import { SearchClient, AzureKeyCredential } from '@azure/search-documents';
const searchClient = new SearchClient(
process.env.AZURE_SEARCH_ENDPOINT!,
process.env.AZURE_SEARCH_INDEX!,
new AzureKeyCredential(process.env.AZURE_SEARCH_KEY!)
);
export async function searchKnowledgeBase(
query: string,
topK: number = 5
): Promise<string[]> {
const results = await searchClient.search(query, {
top: topK,
select: ['content', 'title', 'source'],
queryType: 'semantic',
semanticConfiguration: 'default'
});
const documents: string[] = [];
for await (const result of results.results) {
documents.push(`${result.document.title}: ${result.document.content}`);
}
return documents;
}
RAG-Enhanced Claude Response
// src/rag/claude-rag.ts
import Anthropic from '@anthropic-ai/sdk';
import { searchKnowledgeBase } from './azure-search';
const anthropic = new Anthropic();
export async function getRAGResponse(userQuery: string): Promise<string> {
// 1. Search knowledge base
const relevantDocs = await searchKnowledgeBase(userQuery);
// 2. Build context
const context = relevantDocs.join('\n\n---\n\n');
// 3. Generate response with context
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: `You are a helpful assistant for Teams. Answer questions based on the provided context.
If the context doesn't contain relevant information, say so and provide a general response.
Always cite your sources when using information from the context.`,
messages: [
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${userQuery}`
}
]
});
return response.content[0].type === 'text' ? response.content[0].text : '';
}
Deployment
Azure Bot Service Setup
# Create resource group
az group create --name rg-teams-bot --location eastus
# Create App Service plan
az appservice plan create \
--name asp-teams-bot \
--resource-group rg-teams-bot \
--sku B1 \
--is-linux
# Create Web App
az webapp create \
--name my-teams-bot \
--resource-group rg-teams-bot \
--plan asp-teams-bot \
--runtime "NODE:18-lts"
# Create Bot Channels Registration
az bot create \
--resource-group rg-teams-bot \
--name my-teams-bot \
--kind registration \
--endpoint https://my-teams-bot.azurewebsites.net/api/messages \
--sku F0
# Enable Teams channel
az bot msteams create \
--name my-teams-bot \
--resource-group rg-teams-bot
Environment Variables
# .env
# Azure Bot
BOT_ID=your-bot-id
BOT_PASSWORD=your-bot-password
BOT_TENANT_ID=your-tenant-id
# Azure OpenAI
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-4
# Or OpenAI
OPENAI_API_KEY=sk-xxx
# Or Anthropic
ANTHROPIC_API_KEY=sk-ant-xxx
# Microsoft Graph
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_TENANT_ID=your-tenant-id
# Azure AI Search (for RAG)
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net
AZURE_SEARCH_KEY=your-key
AZURE_SEARCH_INDEX=knowledge-base
Docker Deployment
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3978
CMD ["node", "dist/index.js"]
# docker-compose.yml
version: '3.8'
services:
teams-bot:
build: .
ports:
- "3978:3978"
environment:
- BOT_ID=${BOT_ID}
- BOT_PASSWORD=${BOT_PASSWORD}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
restart: unless-stopped
Teams Toolkit Deployment
# Login to Azure
npx teamsfx account login azure
# Provision resources
npx teamsfx provision --env dev
# Deploy
npx teamsfx deploy --env dev
# Publish to Teams
npx teamsfx publish --env dev
Testing
Local Testing with ngrok
# Start ngrok tunnel
ngrok http 3978
# Update manifest with ngrok URL
# Bot endpoint: https://xxxx.ngrok.io/api/messages
Teams Toolkit Local Debug
# Start local debugging (opens Teams with your app)
npx teamsfx preview --local
Unit Testing
// tests/bot.test.ts
import { TestAdapter, TurnContext } from 'botbuilder';
import { createWelcomeCard } from '../src/cards/welcome-card';
describe('Bot Tests', () => {
let adapter: TestAdapter;
beforeEach(() => {
adapter = new TestAdapter();
});
test('should respond to hello', async () => {
await adapter
.send('hello')
.assertReply((activity) => {
expect(activity.text).toContain('Hello');
});
});
test('should create welcome card', () => {
const card = createWelcomeCard('John');
expect(card.content.body[0].text).toContain('John');
});
});
Best Practices
Conversation Design
┌─────────────────────────────────────────────────────────────────┐
│ CONVERSATION UX GUIDELINES │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 1. GREET INTELLIGENTLY │
│ - Welcome new users with onboarding card │
│ - Return users get quick access to recent actions │
│ │
│ 2. HANDLE ERRORS GRACEFULLY │
│ - Never show stack traces to users │
│ - Provide clear recovery options │
│ - Log errors for debugging │
│ │
│ 3. USE CARDS FOR RICH CONTENT │
│ - Adaptive Cards for forms and structured data │
│ - Hero Cards for simple actions │
│ - Keep cards concise and actionable │
│ │
│ 4. TYPING INDICATORS │
│ - Show typing for long operations │
│ - Provide progress updates for very long tasks │
│ │
│ 5. CONTEXT AWARENESS │
│ - Remember conversation history │
│ - Personalize based on user preferences │
│ - Respect team/channel context │
└─────────────────────────────────────────────────────────────────┘
Security Checklist
Performance Tips
| Tip |
Description |
| Cache Graph tokens |
Token refresh is expensive |
| Stream long responses |
Use typing indicator + chunked responses |
| Index knowledge base |
Pre-embed documents for RAG |
| Use connection pooling |
Reuse HTTP connections |
| Compress payloads |
Gzip large card responses |
Project Templates
AI Assistant Template
// Complete AI assistant with Claude
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';
import { createWelcomeCard } from './cards/welcome-card';
import { createAIResponseCard } from './cards/ai-response-card';
const anthropic = new Anthropic();
const app = new App({ plugins: [new HttpPlugin()] });
const conversations = new Map<string, Anthropic.MessageParam[]>();
// Welcome new users
app.conversationUpdate('membersAdded', async (context) => {
for (const member of context.activity.membersAdded || []) {
if (member.id !== context.activity.recipient.id) {
await context.sendActivity({
attachments: [createWelcomeCard(member.name || 'User')]
});
}
}
});
// Handle messages
app.on('message', async (context) => {
const userId = context.activity.from.id;
const userMessage = context.activity.text;
// Initialize or get conversation
if (!conversations.has(userId)) {
conversations.set(userId, []);
}
const history = conversations.get(userId)!;
history.push({ role: 'user', content: userMessage });
// Show typing
await context.sendActivity({ type: 'typing' });
// Get AI response
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: 'You are a helpful Teams assistant.',
messages: history
});
const answer = response.content[0].type === 'text'
? response.content[0].text
: '';
history.push({ role: 'assistant', content: answer });
// Send rich card response
await context.sendActivity({
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: createAIResponseCard(userMessage, answer)
}]
});
});
// Handle card actions
app.on('adaptiveCard/action', async (context) => {
const action = context.activity.value?.action;
switch (action) {
case 'feedback':
// Log feedback
console.log('Feedback:', context.activity.value);
await context.sendActivity('Thanks for your feedback!');
break;
case 'followUp':
await context.sendActivity('What would you like to know more about?');
break;
}
});
app.start();
Troubleshooting
| Issue |
Cause |
Fix |
| Bot not responding |
Endpoint unreachable |
Check ngrok/Azure URL in manifest |
| Auth failures |
Token expired/invalid |
Refresh OAuth connection |
| Cards not rendering |
Invalid schema |
Validate at adaptivecards.io/designer |
| Graph 403 errors |
Missing permissions |
Check app registration permissions |
| Slow responses |
API latency |
Add typing indicator, consider streaming |
Resources