Awesome-omni-skill ms-teams-apps
Microsoft Teams bots and AI agents - Claude/OpenAI, Adaptive Cards, Graph API
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/ai-agents/ms-teams-apps" ~/.claude/skills/diegosouzapw-awesome-omni-skill-ms-teams-apps && rm -rf "$T"
manifest:
skills/ai-agents/ms-teams-apps/SKILL.mdsafety · automated scan (medium risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- global npm install
- references .env files
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
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
- Validate all incoming messages
- Use App-Only auth for Graph API when possible
- Never log sensitive user data
- Implement rate limiting
- Use managed identity in Azure
- Rotate secrets regularly
- Enable audit logging
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 |