# Reply to messages with Anthropic API (using threads) 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 ```javascript 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 ```plainText routes/zoom-webhookHandler.js ``` ### Code Snippet ```JavaScript 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; ```