Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Listener와 AIP로 Slack 챗봇 만들기

가상 예제인 Tennis Bot은 Slack 직접 메시지로 받은 테니스 질문을 AIP 챗봇에 전달하고, 생성한 응답을 같은 Slack 대화에 보냅니다. 수신용 Slack Listener, 응답 전송용 Slack 소스, AIP 챗봇 함수, 이벤트 처리 함수와 Automate를 연결합니다. Microsoft Teams의 Microsoft Bot Framework 같은 다른 플랫폼에도 같은 구성 원리를 적용할 수 있지만, Listener 유형과 응답 API는 해당 플랫폼에 맞춰야 합니다.

Slack 앱과 수신 이벤트

Slack Listener 설정에 따라 앱을 만들고 Signing Secret을 Listener에 등록합니다. Listener를 활성화한 뒤 Slack의 요청 URL 검증을 완료합니다. 이 예제는 직접 메시지를 받으므로 다음 권한과 이벤트가 필요합니다.

설정값과 용도
OAuth scopeim:history: 직접 메시지 이벤트를 읽습니다.
OAuth scopechat:write: 앱으로 응답 메시지를 보냅니다.
Bot eventmessage.im: 앱에 들어오는 직접 메시지를 구독합니다.
App HomeFeatures > App Home에서 사용자가 봇에게 직접 메시지를 보낼 수 있도록 설정합니다.

앱을 Slack 워크스페이스에 설치합니다. 권한 범위를 바꿨다면 다시 설치해야 합니다. Listener의 Test에서 이벤트 수신을 확인합니다. 테스트 중 전송한 이벤트도 출력 스트림에 저장됩니다.

응답을 전송할 Slack 소스

Data Connection에서 Slack 소스를 생성합니다. Slack 앱 설치 후 Installed App Settings에서 확인한 Bot Bearer 토큰을 인증 정보로 사용합니다. 수신 서명을 검증하는 Signing Secret과 응답 API를 인증하는 Bearer 토큰을 서로 바꾸어 넣지 않습니다.

소스의 Connection settings에서 내보내기와 Code Repositories 가져오기를 허용합니다. Code import configuration에는 코드에서 사용할 API 이름도 설정합니다. 아래 코드에서는 생성된 소스 식별자를 TennisBotSlackSource로 사용합니다. HTTPS 연결은 https://slack.com을 대상으로 하고 외부 통신 정책에서 해당 호스트의 포트 443을 허용합니다.

AIP 챗봇 구성과 함수 게시

Files의 + New > AIP Chatbot 또는 Chatbot Studio의 New AIP Chatbot으로 챗봇을 생성하고 사용할 모델을 선택합니다. AIP Chatbot Studio는 이전에 Agent Studio라는 이름을 사용했습니다. 시스템 지침에는 다음과 같은 내용을 넣을 수 있습니다.

테니스에 관한 질문에 답하는 도우미입니다.
테니스와 관련 없는 질문에는 테니스 질문에 답하는 용도임을 정중하게 설명하고,
사용자가 물어볼 만한 테니스 주제를 제안하세요.
답변은 짧고 친절하게 작성하세요.

검색 문맥에는 실제로 사용할 테니스 규칙 PDF를 문서 문맥으로 제공합니다. 챗봇은 질문에 답할 때 이 내용을 검색할 수 있습니다. Slack으로 보낼 응답에 문서의 XML 인용이나 Foundry 내부 문서 링크를 포함하지 않도록 인용 기능을 끕니다.

Chatbot Studio에서 대표 질문을 입력해 응답을 확인합니다. 여러 질문을 일관되게 평가하려면 AIP Evals의 평가 모음을 사용할 수 있습니다.

챗봇을 함수로 게시합니다. Publish 옆 설정에서 Publish function from chatbot을 켜고, 온톨로지와 함수 이름 및 API 이름을 지정한 뒤 Publish chatbot and function을 선택합니다. 이 예제의 함수 API 이름은 tennisAgentDocumentContextExample입니다. 함수는 userInput 문자열을 받고, 응답 문자열 markdownResponse와 대화의 sessionRid를 반환합니다. 챗봇 함수 계약에 선택적 입력과 게시 버전을 설명합니다.

TypeScript v1 저장소에 가져오기

TypeScript v1 함수용 Code Repository를 만들고 Resource imports에서 Slack 소스와 게시된 챗봇 함수를 가져옵니다. 이 방식은 생성된 소스 식별자와 Queries를 사용합니다.

코드의 외부 의존 항목은 다음과 같습니다. API 이름을 다르게 정했다면 저장소에 생성된 실제 식별자를 사용합니다.

항목필요한 계약
TennisBotSlackSource가져온 Slack 소스의 생성 식별자입니다. HTTPS 연결 URL, 구성된 클라이언트 및 비밀 정보에 접근합니다.
SLACK_BEARER_TOKEN_AUTHSlack 소스가 보관하는 Bearer 토큰의 비밀 정보 키입니다. 토큰 값을 코드에 넣지 않습니다.
Queries.tennisAgentDocumentContextExample가져온 챗봇 함수입니다. { userInput: string } 입력으로 호출하고 markdownResponse를 사용합니다.
decodedEventPayloadListener가 받은 Slack 이벤트 본문을 디코딩한 JSON 문자열입니다.

이벤트 처리 함수

다음 함수는 봇이 보낸 메시지를 제외하고, 사용자 메시지를 챗봇에 전달한 뒤 Slack의 chat.postMessage로 응답을 보냅니다. 봇 메시지를 제외하지 않으면 자신의 응답을 다시 질문으로 처리하는 순환이 생길 수 있습니다.

import { ExternalSystems, OntologyEditFunction } from "@foundry/functions-api";
import { Queries } from "@foundry/ontology-api";
import { TennisBotSlackSource } from "@foundry/external-systems/sources";

export class MyFunctions {
    @ExternalSystems({ sources: [TennisBotSlackSource] })
    @OntologyEditFunction()
    public async processTennisBotEvent(decodedEventPayload: string): Promise<void> {
        const payload = JSON.parse(decodedEventPayload) as {
            event?: { bot_id?: string; text?: string; channel?: string };
        };
        const message = payload.event;
        if (message == null || message.bot_id != null) {
            return;
        }
        if (typeof message.text !== "string" || typeof message.channel !== "string") {
            return;
        }

        const agentResponse = await Queries.tennisAgentDocumentContextExample({
            userInput: message.text,
        });
        const postMessageUrl = `${TennisBotSlackSource.getHttpsConnection().url}/api/chat.postMessage`;
        const response = await TennisBotSlackSource.fetch(postMessageUrl, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${TennisBotSlackSource.getSecret("SLACK_BEARER_TOKEN_AUTH")}`,
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                channel: message.channel,
                text: agentResponse.markdownResponse,
            }),
        });
        if (!response.ok) {
            throw new Error(`Slack chat.postMessage HTTP ${response.status}`);
        }
        const result = await response.json() as { ok?: boolean; error?: string };
        if (result.ok !== true) {
            throw new Error(`Slack chat.postMessage: ${result.error ?? "ok=true가 없는 응답"}`);
        }
    }
}

소스가 제공하는 fetch는 소스의 연결 설정을 사용하는 클라이언트입니다. 일반 네트워크 클라이언트로 바꾸면 함수 실행 환경의 외부 통신 설정이 동일하게 적용된다고 가정할 수 없습니다.

Slack API는 HTTP 요청이 성공해도 응답 JSON의 ok가 false일 수 있습니다. 위 코드는 HTTP 상태와 ok를 모두 확인하며, missing_scope, channel_not_found, not_in_channel 같은 Slack 오류를 실패로 전달합니다. chat.postMessage 계약에 오류 코드와 메시지 필드가 정의되어 있습니다.

코드를 커밋하고 버전 태그를 만들어 함수를 게시합니다. 이 함수는 외부 시스템에 메시지를 쓰므로 함수 기반 액션에 사용할 @OntologyEditFunction()으로 선언합니다.

액션과 자동화 연결

  1. Ontology Manager에서 함수 기반 액션을 생성하고 processTennisBotEvent의 게시 버전을 선택합니다.

  2. 함수 입력 decodedEventPayload를 문자열 액션 매개변수에 연결합니다.

  3. Automate에서 새 자동화를 만들고 조건으로 Stream을 선택합니다.

  4. 입력 스트림에는 Slack Listener의 출력 스트림을 선택합니다.

  5. 효과에는 앞에서 만든 액션을 선택하고, 문자열 입력에 스트림의 Binary payload 열을 연결합니다. Automate는 Binary 내용을 문자열로 디코딩하여 효과에 전달하므로, 함수가 받는 값은 Base64 표시 문자열이 아니라 Slack JSON 본문입니다.

  6. 실행 설정과 요약을 확인한 뒤 자동화를 생성합니다.

스트리밍 데이터셋을 직접 감시하는 Automate 조건은 초당 200레코드를 초과하지 않는 입력에 사용합니다. 앞단 HTTPS Listener의 대략 초당 100건 제한도 별도로 적용됩니다. Automate의 처리는 최소 한 번 방식이므로 같은 이벤트가 다시 처리되면 응답이 중복 전송될 수 있습니다. 효과 실행의 순서가 필요하면 실행을 큐에 넣는 설정을 사용합니다.

Slack에서 봇에게 직접 메시지를 보내고 Listener 수신, 자동화 실행, Slack 응답을 차례로 확인합니다. 응답이 없다면 앱의 이벤트 구독과 재설치 여부, 함수 버전, 소스의 내보내기 및 코드 가져오기 권한, Slack API 오류를 구분하여 확인합니다.

대화와 응답 방식 확장

응답이 생성되는 동안 대기 상태를 보여 주려면 먼저 접수 메시지를 보내고, 생성이 끝났을 때 해당 Slack 메시지를 최종 응답으로 갱신할 수 있습니다.

대화 문맥을 이어가려면 챗봇 함수의 첫 응답에서 받은 sessionRid를 대화에 대응하는 온톨로지 객체에 저장합니다. 다음 메시지에서는 그 객체를 조회하여 동일한 RID를 함수 입력에 전달합니다. 새 세션을 만들 때는 sessionRid를 생략하고 빈 문자열을 보내지 않습니다. 위의 기본 코드는 RID를 전달하지 않으므로 매 요청을 새 세션으로 시작합니다.

더 낮은 지연이 필요하면 Automate의 액션 호출 대신 스트리밍 파이프라인의 외부 UDF로 처리 경로를 구성할 수 있습니다. 상태 유지와 처리 보장을 포함한 선택 기준은 이벤트 처리 방식에 있습니다.