In this section, you'll integrate the Anthropic API with your Zoom Chatbot to handle user messages in threads. When a user sends a message to the bot, your app will generate an AI-powered reply (using models like Claude 3.5) and respond in the same thread.
By the end of this section, you will have:
- A working integration with the Anthropic API
- Threaded AI responses in Chat
- Session-based message history for improved conversational context
Add Anthropic integration
Create a utility function that sends a user's message to Anthropic, receives a completion response, and replies to the chat thread. This function maintains per-user conversation history to support contextual conversations.
File Path
utils/anthropic.js
Code Snippet
import dotenv from 'dotenv';
import { sendChatMessage } from './zoom-api.js';
dotenv.config();
let conversationHistory = new Map();
const decoder = new TextDecoder();
let buf = '';
/**
* Main entry point for interacting with Anthropic API
* @param {Object} payload - Zoom webhook payload containing toJid / message
* @param {Object} options - { stream?: boolean, onStreamChunk?: (chunk, soFar) => void }
*/
export async function callAnthropicAPI(payload, options = {}) {
const userJid = payload?.toJid;
if (!userJid) {
console.error('Error: payload.toJid is missing.');
return;
}
try {
const { stream = true, onStreamChunk = null } = options;
const headers = buildHeaders();
// Build conversation history
const history = conversationHistory.get(userJid) || [];
const userMessage = { role: 'user', content: payload.cmd || payload.message || 'Hello' };
history.push(userMessage);
const requestData = buildRequestBody({
model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-latest',
messages: history,
stream,
});
console.log(`Sending message to Anthropic (model=${requestData.model}, stream=${stream}) for user: ${userJid}`);
const completion = stream
? await handleStreamingResponse(requestData, headers, userJid, payload, onStreamChunk)
: await handleNonStreamingResponse(requestData, headers, userJid, payload);
return completion;
} catch (error) {
console.error('Error in callAnthropicAPI:', error?.message || error);
try {
await sendChatMessage(
payload?.toJid,
'Sorry, I hit an AI model error. I will be back shortly. (Check model access/config.)',
payload?.reply_to || null
);
} catch (sendError) {
console.error('Failed to send error message to user:', sendError?.message || sendError);
}
throw error;
}
}
function buildHeaders() {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) throw new Error('Missing ANTHROPIC_API_KEY in environment variables.');
if (!apiKey.startsWith('sk-ant-')) throw new Error('Invalid ANTHROPIC_API_KEY format. Should start with "sk-ant-".');
return {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': apiKey,
};
}
function buildRequestBody({ model, messages, stream }) {
return {
model,
messages,
system: 'You are a helpful AI assistant integrated with Chat. Provide concise, helpful responses to user questions and requests.',
temperature: 0.7,
max_tokens: 1000,
};
}
/**
* Non-streaming path using native fetch.
*/
async function handleNonStreamingResponse(requestData, headers, userJid, payload) {
const ANTHROPIC_URL = '[https://api.anthropic.com/v1/messages](https://api.anthropic.com/v1/messages)';
const response = await fetch(ANTHROPIC_URL, {
method: 'POST',
headers,
body: JSON.stringify({ ...requestData, stream: false }),
});
if (!response.ok) {
let errorData = {};
try {
errorData = await response.json();
} catch {
errorData = { message: await response.text() };
}
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = { data: errorData };
throw error;
}
const data = await response.json();
if (!data?.content || !Array.isArray(data.content)) {
throw new Error(`Unexpected response from Anthropic API: ${JSON.stringify(data)}`);
}
const completion = data.content
.filter((b) => b.type === 'text')
.map((b) => b.text)
.join('\n')
.trim();
if (!completion) throw new Error('Empty response from Anthropic API');
// Append assistant message & trim history
const history = conversationHistory.get(userJid) || [];
history.push({ role: 'assistant', content: completion });
conversationHistory.set(userJid, history.length > 20 ? history.slice(-20) : history);
await sendChatMessage(userJid, completion, payload.reply_to || null);
console.log('Successfully sent response to Chat');
return completion;
}
/**
* Streaming path using native fetch + Web Streams.
* Parses Server-Sent Events (SSE): frames separated by blank line, with "event:" and "data:" lines.
*/
async function handleStreamingResponse(requestData, headers, userJid, payload, onStreamChunk) {
const ANTHROPIC_URL = '[https://api.anthropic.com/v1/messages](https://api.anthropic.com/v1/messages)';
const sseHeaders = { ...headers, Accept: 'text/event-stream' };
const response = await fetch(ANTHROPIC_URL, {
method: 'POST',
headers: sseHeaders,
body: JSON.stringify({ ...requestData, stream: true }),
});
if (!response.ok) {
let errorData = {};
try {
errorData = await response.json();
} catch {
errorData = { message: await response.text() };
}
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = { data: errorData };
throw error;
}
const reader = response.body.getReader();
let completion = '';
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
// SSE frames are separated by a blank line (\n\n)
let sep;
while ((sep = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, sep);
buf = buf.slice(sep + 2);
let eventType = '';
let dataLine = '';
for (const line of frame.split('\n')) {
if (line.startsWith('event:')) eventType = line.slice(6).trim();
else if (line.startsWith('data:')) dataLine += line.slice(5).trim();
}
if (!dataLine) continue;
if (dataLine === '[DONE]') continue;
try {
const parsed = JSON.parse(dataLine);
if (parsed.type === 'content_block_delta' && parsed.delta?.type === 'text_delta') {
const chunk = parsed.delta.text || '';
if (chunk) {
completion += chunk;
onStreamChunk?.(chunk, completion);
}
}
if (parsed.type === 'message_stop' || eventType === 'message_stop') {
break;
}
} catch {
// Ignore malformed pieces
}
}
}
} finally {
reader.releaseLock();
}
if (!completion.trim()) {
buf = '';
throw new Error('Empty response from Anthropic streaming API');
}
// Append assistant message & trim history
const history = conversationHistory.get(userJid) || [];
history.push({ role: 'assistant', content: completion });
conversationHistory.set(userJid, history.length > 20 ? history.slice(-20) : history);
// Send the full completion to Zoom
await sendChatMessage(userJid, completion, payload.reply_to || null);
console.log('Successfully sent streaming response to Chat');
buf = '';
return completion;
}
export async function callAnthropicAPIStreaming(payload, onStreamChunk) {
return callAnthropicAPI(payload, {
stream: true,
onStreamChunk,
});
}
export function getConversationHistory(userJid) {
return conversationHistory.get(userJid) || [];
}
export function clearConversationHistory(userJid) {
conversationHistory.delete(userJid);
console.log(`Cleared conversation history for user: ${userJid}`);
}
Update webhook to trigger Anthropic replies
Modify the bot_notification case in your webhook handler to call the Anthropic API whenever a user sends a message to the chatbot.
This implementation enables your Zoom chatbot to respond in threads using AI-generated answers, creating a conversational experience powered by Anthropic's large language models.
File Path
routes/zoom-webhookHandler.js
Code Snippet
import { callAnthropicAPI } from '../utils/anthropic.js';
// ... inside your webhook router switch statement ...
case 'bot_notification':
console.log('Processing bot notification from Zoom Team Chat');
await callAnthropicAPI(payload, true);
break;