직접 만든 AI 에이전트를 MS Teams로 가져오기
이미 구축된 다양한 AI 에이전트(Slack 봇, LangChain 체인 등)를 마이크로소프트 Teams 환경으로 쉽게 통합하는 방법을 소개합니다. Teams TypeScript SDK의 HTTP 서버 어댑터 패턴을 활용해 기존 코드베이스를 그대로 유지하면서 Teams 메시징 엔드포인트(/api/messages)만 추가할 수 있습니다. 이를 통해 개발자는 에이전트 로직을 중복으로 작성할 필요 없이, 실무자들이 주로 활용하는 Teams 환경에 즉각적으로 AI 기능을 투입할 수 있습니다.
당신은 이미 에이전트를 구축했습니다. 그리고 그것은 어딘가에 존재합니다. LangChain 체인일 수도, Azure AI Foundry 배포일 수도, 혹은 Slack 봇일 수도 있습니다. 하지만 실제 사용자들은 Teams 안에서 생활합니다. 대부분의 기업 업무가 Teams에서 이루어지며, 의사결정이 이루어지고, 고객의 문의에 답하며, 프로젝트가 진행됩니다. Teams에 특화된 무언가를 새로 구축하기 전에, 당신의 에이전트를 바로 그 업무 환경에 통합하는 것은 충분히 가치 있는 일입니다.
이 문제는 Teams TypeScript SDK의 단일 패턴인 'HTTP 서버 어댑터(HTTP server adapter)'로 해결할 수 있습니다. 이 어댑터를 기존 HTTP 서버에 연결하면 메시징 엔드포인트를 등록해 주며, 기존 서버는 그대로 계속 실행됩니다. 아래의 시나리오들은 세 가지 다른 시작점(Slack 봇, LangChain 체인, Azure AI Foundry 에이전트)을 다룹니다. 또한 이 SDK는 개발자가 신경 쓰고 싶지 않은 부분들도 알아서 처리해 줍니다. 들어오는 모든 요청이 합법적으로 Teams로부터 온 것인지 핸들러를 호출하기 전에 검증하며, 메시지를 알맞은 이벤트 핸들러로 자동 라우팅합니다.
이 패턴 (The Pattern)
이 글의 모든 예시는 동일한 세 단계 구조를 따릅니다:
import { App as TeamsApp, ExpressAdapter } from '@microsoft/teams.apps';
const adapter = new ExpressAdapter(expressApp); // 1. 서버를 래핑(Wrap)합니다.
const teamsApp = new TeamsApp({ httpServerAdapter: adapter }); // 2. 앱을 생성합니다.
teamsApp.on('message', async ({ send, activity }) => { // 3. 메시지를 처리합니다.
await send(/* 에이전트의 응답 */);
});
await teamsApp.initialize(); // 당신의 서버에 POST /api/messages 엔드포인트를 등록합니다.
SDK는 기존 Express 앱에 POST /api/messages 라우트를 주입합니다. /api/messages는 Teams가 봇에게 메시지를 전달하기 위해 사용하는 잘 알려진 엔드포인트이자, HTTP 서버가 갖춰야 할 Teams 형태의 인터페이스입니다. 서버의 주도권은 당신에게 그대로 남아있으며, Teams SDK는 단지 그 하나의 엔드포인트만 추가할 뿐입니다.
시나리오 1: Slack 봇 (Slack Bot)
Bolt(또는 웹 서비스로 배포된 다른 종류의 봇)으로 구축한 Slack 봇이 있습니다. 팀에서 Slack과 Teams를 모두 사용하고 있으며, 두 개의 코드베이스를 유지하는 대신 동일한 Express 서버에서 두 가지를 모두 실행하고 싶을 것입니다. ExpressReceiver를 사용하면 Bolt가 서버를 소유하는 대신 당신의 Express 앱에 마운트되도록 할 수 있습니다. Teams SDK도 마찬가지 방식을 사용하므로, 두 플랫폼이 동일한 프로세스를 공유하게 됩니다.
slack-app.ts: 기존의 Slack 로직 (그대로 유지)
import { App as BoltApp, ExpressReceiver } from '@slack/bolt';
import type { Express } from 'express';
export function mountSlack(expressApp: Express) {
const slackReceiver = new ExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
app: expressApp,
endpoints: { events: '/slack/events' },
});
const slackApp = new BoltApp({
token: process.env.SLACK_BOT_TOKEN,
receiver: slackReceiver,
});
slackApp.message('hello', async ({ say }) => {
await say('Hey! Caught you on Slack.');
});
}
teams-app.ts:
import express from 'express';
import { App as TeamsApp, ExpressAdapter } from '@microsoft/teams.apps';
import { mountSlack } from './slack-app';
const expressApp = express();
mountSlack(expressApp);
// Teams는 /api/messages에 마운트됩니다.
const adapter = new ExpressAdapter(expressApp);
const teamsApp = new TeamsApp({ httpServerAdapter: adapter });
teamsApp.on('message', async ({ send, activity }) => {
await send(`Hey ${activity.from.name}! You said: "${activity.text}"`);
});
export { expressApp, teamsApp };
두 플랫폼 모두 동일한 프로세스 내에서 실행됩니다. Slack은 /slack/events에 도달하고, Teams는 /api/messages에 도달합니다. 공유되는 에이전트 로직(LLM 호출, 데이터베이스 조회, 비즈니스 규칙 등)은 두 핸들러가 모두 호출하는 일반 함수 형태로 존재하게 됩니다.
시나리오 2: LangChain
당신은 LangChain 체인을 가지고 있으며, Teams 사용자들이 그것과 대화하기를 원합니다.
chain.ts: 기존의 LangChain 로직 (그대로 유지)
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
let _chain: ReturnType<typeof buildChain