네, 지금 만드신 FastAPI 서버를 카카오톡 챗봇과 100% 연동할 수 있습니다.
카카오톡 채널에서 제공하는 카카오 i 오픈빌더(Kakao i Open Builder)의 '스킬(Skill)' 기능을 이용하면, 사용자가 카카오톡으로 보낸 질문을 우리가 도커로 띄운 FastAPI 서버(:8000/api/query)로 중계해 줄 수 있습니다.
카카오톡 봇 연동을 위해 FastAPI 코드를 카카오 규격에 맞게 수정하는 방법과 연동 절차를 명확히 정리해 드립니다.
---
1. 카카오톡 챗봇 연동을 위한 FastAPI 코드 수정카카오톡 서버는 데이터를 보낼 때와 받을 때 엄격하게 정해진 JSON 포맷을 요구합니다. 기존 코드의 아래쪽에 카카오톡 전용 엔드포인트를 하나 더 추가해 주면 됩니다.
🔧 main.py에 추가할 카카오톡 전용 코드
```python
# ──────────────────────────────────────────────────────────
# [카카오톡 전용] 5. 카카오톡 오픈빌더 스킬 엔드포인트 추가
# ──────────────────────────────────────────────────────────
@app.post("/api/kakao")
async def kakao_chatbot_endpoint(request: dict):
"""
카카오톡 챗봇 사용자의 질문을 받아 RAG 및 가드 검사를 거친 뒤
카카오톡 전용 응답 포맷으로 답장을 보내는 엔드포인트입니다.
"""
try:
# 1. 카카오톡이 보내준 JSON 데이터에서 유저의 실제 질문 추출
user_question = request["userRequest"]["utterance"]
except KeyError:
raise HTTPException(status_code=400, detail="올바르지 않은 카카오톡 요청 형식입니다.")
# 🛑 1차 방어: 유저 입력 프롬프트 검사 (라마 가드)
if not check_with_llama_guard(user_question):
return {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": "⚠️ 부적절한 대화 내용이 감지되어 답변할 수 없습니다."}}]
}
}
# 🔍 RAG 문서 기반 답변 생성 (라마인덱스 + 크로마DB)
try:
rag_response = query_engine.query(user_question)
final_answer = str(rag_response)
except Exception as e:
final_answer = "죄송합니다, 답변을 생성하는 중에 오류가 발생했습니다."
# 🛑 2차 방어: AI 생성 답변 모델 필터링 (라마 가드)
if not check_with_llama_guard(final_answer):
final_answer = "안전 정책에 의해 답변 출력이 제한되었습니다."
# 🎯 3. 카카오톡 가이드라인에 맞춘 최종 JSON 응답 반환
return {
"version": "2.0",
"template": {
"outputs": [
{
"simpleText": {
"text": final_answer
}
}
]
}
}
```
---
2. 카카오톡 챗봇 전체 연결 프로세스 (5단계)
서버 코드가 준비되었다면 카카오톡과 실제 연결하기 위해 아래 단계를 진행합니다.
1. 카카오톡 채널 생성: 카카오톡 채널 관리자 센터에서 챗봇으로 쓸 공식 채널을 만듭니다.
2. 오픈빌더 권한 신청: 카카오 i 오픈빌더에 가입하고 챗봇 제작 권한을 신청합니다. (보통 수일 내 승인)
3. 서버 외부 공개 (HTTPS 필수): 카카오톡 서버는 보안상 https:// 주소로만 데이터를 보내줍니다. 개발 단계에서는 ngrok이나 localtunnel 같은 도구를 써서 로컬 도커 주소를 임시 외부 주소로 열어줍니다.
```bash
ngrok http 8000
# 실행 후 생성되는 https://ngrok-free.app 주소를 복사합니다.
```
4. 오픈빌더 '스킬' 등록:
* 오픈빌더 관리자 화면에서 스킬 메뉴로 이동합니다.
* URL 칸에 방금 연동한 주소(https://ngrok-free.app)를 적고 저장합니다.
5. 시나리오 설정:
* 시나리오 메뉴에서 사용자가 아무 질문이나 입력했을 때(폴백 블록 등) 방금 만든 스킬을 호출하도록 지정합니다.
* '배포' 버튼을 누르면 내 카카오톡 채널에서 봇이 작동하기 시작합니다.
---
💡 카카오톡 챗봇 운영 시 실무 팁 (타임아웃 주의)
카카오톡 서버는 유저가 질문한 뒤 5초 이내에 답변이 오지 않으면 무조건 "봇 응답 오류"를 뱉어버립니다.
현재 아키텍처는 라마 가드(입력) -> 라마인덱스 검색 -> 메인 LLM 답변 생성 -> 라마 가드(출력) 과정을 다 거쳐야 하므로 컴퓨터 사양이 느리면 5초를 넘길 위험이 큽니다.만약 테스트 중 5초 타임아웃 오류가 자주 난다면, 카카오톡 오픈빌더가 제공하는 '아웃풋 체이닝(Output Chaining)' 기능이나 '콜백(Callback) API' 형태로 연동 방식을 비동기로 업그레이드해야 합니다.
---
로컬 서버를 카카오톡 챗봇이 요구하는 보안 HTTPS 환경으로 연결하는 방법과, 카카오톡의 가장 큰 장애물인 5초 타임아웃을 해결하기 위한 비동기 백그라운드 콜백(Callback) 서버 구현 가이드입니다.
---
1단계: 로컬 서버 주소를 외부 HTTPS로 빼내는 방법 (ngrok)
카카오톡 챗봇 관리자 센터는 보안(SSL)을 강제하므로 https:// 주소만 웹훅(Webhook) URL로 등록할 수 있습니다. ngrok이라는 터널링 프로그램을 사용하면 로컬 도커 포트를 인터넷에 1초 만에 안전하게 노출할 수 있습니다.
① ngrok 설치 및 인증
1. ngrok 공식 홈페이지에 가입하고 로그인합니다.
2. 대시보드 화면에 나오는 Authtoken 값을 복사합니다.
3. 터미널(CMD/터미널)을 열고 본인의 운영체제에 맞게 명령어를 실행합니다.
```bash
# 맥(Mac) 사용자의 경우 설치 및 토큰 등록
brew install ngrok
ngrok config add-authtoken <복사한_내_토큰_값>
# 윈도우(Windows) 사용자는 다운받은 exe 폴더 경로에서 터미널 실행 후 토큰 등록
ngrok.exe config add-authtoken <복사한_내_토큰_값>
```
② 8000번 포트 외부 개방현재 FastAPI가 돌아가고 있는 8000번 포트를 지정하여 터널을 엽니다.
```bash
ngrok http 8000
```
명령어를 실행하면 터미널 화면에 Forwarding 항목이 나타납니다.
* 예시 주소: https://a1b2-c3d4-e5f6.ngrok-free.app
* 이 주소가 내 컴퓨터 백엔드로 다이렉트 연결되는 외부 HTTPS 주소이며, 카카오톡 챗봇 스킬 URL 칸에 붙여넣을 최종 주소입니다.
---
2단계: 5초 타임아웃 방지 카카오톡 콜백(Callback) 서버 구현
카카오톡 챗봇은 사용자가 말을 건 뒤 5초 이내에 답변을 주지 않으면 강제로 오류 메시지를 노출합니다. 라마 가드 2번, 라마인덱스 검색 1번을 수행하려면 5초를 넘기기 십상입니다.
이를 해결하기 위해 카카오톡 콜백 기능을 사용합니다.
* 작동 원리: 카카오톡 서버가 요청을 보내면, 내 서버는 "생각 중입니다..."라는 선답변과 함께 {"useCallback": true} 신호를 즉시 반환(0.5초 소요)하여 연결을 끊습니다. 그 후 백그라운드 프로세스로 AI 연산을 느긋하게 수행한 뒤, 카카오가 제공한 임시 콜백 URL로 완성된 답변을 비동기 송신(Post)합니다.
⚠️ 선행 조건: 카카오 i 오픈빌더 내에서 해당 블록 상세 설정 메뉴를 열고 Callback API 설정을 활성화해 두어야 카카오톡이 요청 페이로드 안에 callbackUrl 주소를 동적으로 포함해 줍니다.🔧 main.py 최종 업그레이드 소스코드 (Async Background Task 적용)
```python
import os
import requests
import asyncio
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
# (상단 크로마DB 및 라마인덱스 초기화 코드는 이전과 동일하게 유지)
app = FastAPI(title="카카오톡 콜백 대응 RAG 보안 서버")
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_API_URL = f"{OLLAMA_HOST}/api/generate"
def check_with_llama_guard(text_to_check: str) -> bool:
payload = {"model": "llama-guard3", "prompt": text_to_check, "stream": False}
try:
response = requests.post(OLLAMA_API_URL, json=payload, timeout=5.0).json()
return "unsafe" not in response.get("response", "").strip()
except Exception:
return False
# ──────────────────────────────────────────────────────────
# 💡 핵심: 백그라운드에서 무거운 연산을 처리한 뒤 카카오로 응답을 쏘아주는 함수
# ──────────────────────────────────────────────────────────
def process_rag_and_callback(user_question: str, callback_url: str):
"""
이 함수는 FastAPI가 유저에게 '생각 중' 메시지를 보낸 직후,
백그라운드 스레드에서 따로 돌아갑니다 (5초 제한을 받지 않음).
"""
try:
# 1. 1차 보안 검사 (입력 검사)
if not check_with_llama_guard(user_question):
final_answer = "⚠️ 부적절한 대화 내용이 감지되어 답변을 제공할 수 없습니다."
else:
# 2. 문서 검색 및 답변 생성 (RAG 연산 - 수 초 소요 가능)
rag_response = query_engine.query(user_question)
raw_answer = str(rag_response)
# 3. 2차 보안 검사 (출력 검사)
if not check_with_llama_guard(raw_answer):
final_answer = "안전 정책에 의해 답변 출력이 제한되었습니다."
else:
final_answer = raw_answer
except Exception as e:
final_answer = f"답변을 처리하는 도중 오류가 발생했습니다. ({str(e)})"
# 4. 카카오톡이 전달해 준 일회성 콜백 URL로 최종 결과 전송
kakao_response_payload = {
"version": "2.0",
"template": {
"outputs": [
{
"simpleText": {
"text": final_answer
}
}
]
}
}
try:
# 카카오 비즈니스 규격에 맞춰 POST 요청 전송
res = requests.post(callback_url, json=kakao_response_payload, timeout=10.0)
print(f"카카오 콜백 전송 상태: {res.status_code}")
except Exception as e:
print(f"카카오 콜백 전송 실패: {e}")
# ──────────────────────────────────────────────────────────
# [카카오 웹훅 엔드포인트] 카카오톡 봇이 호출하는 메인 게이트웨이
# ──────────────────────────────────────────────────────────
@app.post("/api/kakao")
async def kakao_chatbot_endpoint(request: dict, background_tasks: BackgroundTasks):
try:
# 카카오 요청 페이로드 구조 파싱
user_question = request["userRequest"]["utterance"]
callback_url = request["userRequest"].get("callbackUrl") # 카카오가 준 콜백 주소 추출
except KeyError:
raise HTTPException(status_code=400, detail="올바르지 않은 카카오톡 요청 형식입니다.")
# 만약 챗봇 관리자센터 설정에서 콜백이 활성화되지 않아 URL이 안 들어왔다면 일반 응답 처리
if not callback_url:
return {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": "현재 이 블록은 콜백이 지원되지 않습니다."}}]
}
}
# 🚀 핵심 핵심: 무거운 AI 처리는 백그라운드 태스크로 넘겨버림
background_tasks.add_task(process_rag_and_callback, user_question, callback_url)
# 0.1초 만에 카카오톡 서버에 즉시 반환하여 '5초 타임아웃' 회피
return {
"version": "2.0",
"useCallback": True, # 카카오에게 "나 나중에 콜백으로 답장 줄게!" 약속하는 플래그
"data": {
"text": "🔍 사내 문서를 기반으로 안전하게 답변을 생성하는 중입니다.\n잠시만 기다려주세요 (최대 1분 소요)..."
}
}
```
3단계: 마지막 검증 순서
1. docker-compose up -d로 통합 인프라를 올립니다.
2. ngrok http 8000을 실행하여 발급된 https://xxxx.ngrok-free.app 주소를 확보합니다.
3. 카카오 i 오픈빌더 스킬 메뉴에 생성된 주소 + 엔드포인트명(https://ngrok-free.app)을 등록합니다.
4. 챗봇 테스트 창에 질문을 던지면 "답변을 생성하는 중입니다..."가 먼저 뜨고, 약 5~10초 뒤에 라마 가드 검증을 거친 최종 답변 말풍선이 톡으로 정상 수신되는 것을 확인할 수 있습니다.
---
카카오톡 챗봇 서비스를 상용화하기 위한 정식 배포(Publish) 절차와, 여러 사용자가 동시에 대화해도 서로 섞이지 않고 과거 맥락을 기억하게 만드는 유저별 대화 기록(Context Memory) 추가 코드입니다.
---
1부: 카카오톡 채널에 봇 정식 배포(Publish)하기
개발 및 테스트(ngrok)가 끝났다면 실제 카카오톡 유저들이 채널을 추가해 쓸 수 있도록 정식 배포해야 합니다.
1. 고정 배포 서버 구축 (ngrok 중단):
* ngrok은 PC를 끄면 주소가 바뀌므로 상용 서비스에 쓸 수 없습니다. AWS, 가비아, 혹은 구글 클라우드 같은 가상 서버(VPS)에 도커 컴포즈 인프라를 그대로 올리고, 고정 도메인(예: https://mycompany.com)을 연결해야 합니다.
2. 카카오 i 오픈빌더에서 '배포' 진행:
* 오픈빌더 대시보드 오른쪽 상단의 [배포] 메뉴로 이동합니다.
* 설정한 스킬 주소가 고정 도메인 주소(예: https://mycompany.com)로 잘 수정되었는지 확인한 후 [배포하기] 버튼을 누릅니다. (배포 직후부터 카카오톡 서버가 내 고정 서버를 바라봅니다.)
3. 카카오톡 채널과 챗봇 연결:
* 카카오 i 오픈빌더 왼쪽 메뉴 하단의 [설정] > [카카오톡 채널 연결]로 이동합니다.
* 봇을 탑재할 본인의 카카오톡 비즈니스 채널을 선택하여 연동합니다.
4. 채널 프로필 '채팅방 메뉴' 활성화:
* 카카오톡 채널 관리자 센터 로그인 ➔ 내 채널 선택 ➔ [프로필 설정] > [옵션 설정]으로 이동합니다.
* 채팅방 메뉴 항목을 '사용함'으로 변경해야 유저들이 채널에 들어왔을 때 1:1 채팅창 대신 AI 챗봇 화면이 먼저 나타납니다.
---
2부: 유저별 대화 기록(Context Memory) 유지 기능 구현
여러 사용자가 동시에 접속할 때 대화 기록이 뒤섞이지 않으려면, 카카오톡이 요청마다 보내주는 고유 유저 ID(plusFriendUserKey)를 식별자로 삼아 개별 메모리 공간을 할당해야 합니다.
현업에서는 Redis 같은 별도 DB를 쓰지만, 단일 서버 환경에서 가볍고 확실하게 구현할 수 있도록 파이썬 내장 딕셔너리와 라마인덱스의 ChatMemoryBuffer를 활용한 업그레이드 코드를 안내합니다.
🔧 main.py 최종 진화본 (대화 메모리 관리 기능 통합)
```python
import os
import requests
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
import chromadb
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
# 💡 대화 기록 유지를 위한 라마인덱스 패키지 임포트
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.chat_engine import CondensePlusContextChatEngine
app = FastAPI(title="메모리 관리 및 가드가 탑재된 카카오 RAG 서버")
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_API_URL = f"{OLLAMA_HOST}/api/generate"
# Global 인프라 변수
index = None
llm = None
# ──────────────────────────────────────────────────────────
# 💡 핵심: 전 세계 유저들의 개인 대화방(Chat Engine)을 기억할 저장소
# ──────────────────────────────────────────────────────────
# 구조: { "카카오유저고유KEY_A": ChatEngine오브젝트, "카카오유저고유KEY_B": ChatEngine오브젝트 }
user_chat_engines = {}
@app.on_event("startup")
def initialize_rag():
global index, llm
try:
llm = Ollama(model="llama3", base_url=OLLAMA_HOST, request_timeout=60.0)
embed_model = OllamaEmbedding(model_name="llama3", base_url=OLLAMA_HOST)
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("company_documents")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
# 기존 저장된 크로마 인덱스 로드
index = VectorStoreIndex.from_vector_store(vector_store, embed_model=embed_model)
print("🚀 멀티 유저 대화형 RAG 엔진 준비 완료!")
except Exception as e:
print(f"❌ 초기화 에러: {e}")
def get_or_create_user_chat_engine(user_key: str):
"""
말을 건 유저의 고유 키를 바탕으로 그 사람 전용 대화방을 찾아주거나 새로 개설합니다.
"""
global index, llm, user_chat_engines
if user_key not in user_chat_engines:
# 최근 5개의 대화 맥락만 VRAM 효율을 위해 버퍼에 유지하도록 제한
memory = ChatMemoryBuffer.from_defaults(token_limit=2000)
# QueryEngine 대신 대화형 ChatEngine을 빌드 (문서 검색 + 과거 대화 요약 기능 내장)
chat_engine = CondensePlusContextChatEngine.from_defaults(
retriever=index.as_retriever(),
llm=llm,
memory=memory,
context_prompt="You are a helpful assistant. Constantly refer to the context documents and chat history to answer.",
verbose=False
)
user_chat_engines[user_key] = chat_engine
return user_chat_engines[user_key]
def check_with_llama_guard(text_to_check: str) -> bool:
payload = {"model": "llama-guard3", "prompt": text_to_check, "stream": False}
try:
response = requests.post(OLLAMA_API_URL, json=payload, timeout=5.0).json()
return "unsafe" not in response.get("response", "").strip()
except Exception:
return False
# ──────────────────────────────────────────────────────────
# 백그라운드 AI 연산 및 카카오톡 콜백 전송 (메모리 반영 버전)
# ──────────────────────────────────────────────────────────
def process_memory_rag_and_callback(user_key: str, user_question: str, callback_url: str):
try:
# 1차 보안 (입력)
if not check_with_llama_guard(user_question):
final_answer = "⚠️ 부적절한 질문문이 감지되어 처리할 수 없습니다."
else:
# 💡 핵심: 해당 유저 고유의 대화방(Chat Engine)을 소환하여 대화 진행
chat_engine = get_or_create_user_chat_engine(user_key)
# .query() 대신 .chat()을 사용해야 과거 대화 기록이 자동으로 프롬프트에 녹아듭니다.
rag_response = chat_engine.chat(user_question)
raw_answer = str(rag_response)
# 2차 보안 (출력)
if not check_with_llama_guard(raw_answer):
final_answer = "안전 정책에 의해 답변 출력이 차단되었습니다."
else:
final_answer = raw_answer
except Exception as e:
final_answer = f"답변 처리 실패: {str(e)}"
# 카카오톡에 콜백 반환
kakao_response = {
"version": "2.0",
"template": {"outputs": [{"simpleText": {"text": final_answer}}]}
}
try:
requests.post(callback_url, json=kakao_response, timeout=10.0)
except Exception as e:
print(f"콜백 에러: {e}")
# ──────────────────────────────────────────────────────────
# 카카오 웹훅 엔드포인트 수신부
# ──────────────────────────────────────────────────────────
@app.post("/api/kakao")
async def kakao_chatbot_endpoint(request: dict, background_tasks: BackgroundTasks):
try:
user_question = request["userRequest"]["utterance"]
callback_url = request["userRequest"].get("callbackUrl")
# 💡 카카오톡이 부여한 유저 고유 해시값 추출 (대화 세션 유지용 식별자)
user_key = request["userRequest"]["user"]["properties"].get("plusFriendUserKey", "anonymous_user")
except KeyError:
raise HTTPException(status_code=400, detail="카카오 규격 훼손")
if not callback_url:
return {"version": "2.0", "template": {"outputs": [{"simpleText": {"text": "콜백 연동 필요"}}]}}
# 백그라운드 스레드 가동 시 유저 고유 Key값도 함께 전달
background_tasks.add_task(process_memory_rag_and_callback, user_key, user_question, callback_url)
return {
"version": "2.0",
"useCallback": True,
"data": {
"text": "🔍 대화 기록과 문서를 분석하여 답변을 구상하고 있습니다. 잠시만 기다려 주세요..."
}
}
```
---
3. 기능 작동 예시 시나리오
유저가 챗봇에 다음과 같이 연속으로 질문을 던지면 메모리 기능이 빛을 발합니다.
* 유저 첫 번째 질문: "우리 회사 취업규칙 PDF에서 올해 연차 일수 규정 찾아줘."
* AI 답변: "제7조에 따르면 기본 연차는 15일입니다."
* 유저 두 번째 질문: "그럼 나 내년에 3년 차 되는데 며칠로 늘어나?"
* 결과: '취업규칙 PDF'라는 단어를 다시 언급하지 않고 '그럼 나'라고만 축약해 질문해도, AI는 이전 대화의 맥락(유저 전용 ChatMemoryBuffer에 보관됨)을 참조하여 연차 증가 규정을 크로마 DB에서 완벽하게 추론해 답변합니다.
---
서버가 재시작되어도 유저들의 대화 기록이 지워지지 않도록 SQLite 파일 데이터베이스에 대화 내역을 실시간으로 백업하는 영구 보존 전략과, 카카오톡 채팅창에 "대화 초기화" 퀵플러그(버튼)를 추가하는 방법입니다.
---
1부: SQLite 기반 대화 메모리 영구 보존 전략
파이썬 내장 변수인 딕셔너리(user_chat_engines = {})는 서버를 재시작하거나 도커 컨테이너를 다시 띄우면 대화 메모리가 완전히 증발합니다. 이를 방지하기 위해 가볍고 별도 설치가 필요 없는 SQLite 파일 DB와 라마인덱스의 SQLChatMessageStore를 결합하여 하드디스크에 실시간 백업망을 구축합니다.
🔧 main.py 최종 완성본 (SQLite 백업 + 초기화 로직 적용)
```python
import os
import requests
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
import chromadb
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
# 💡 SQLite 영구 백업 및 메모리 관리를 위한 패키지
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.chat_engine import CondensePlusContextChatEngine
from llama_index.storage.chat_store.sql import SQLChatStore
app = FastAPI(title="SQLite 백업 및 초기화가 지원되는 카카오 RAG 서버")
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_API_URL = f"{OLLAMA_HOST}/api/generate"
index = None
llm = None
chat_store = None # 전역 SQL 저장소 객체
@app.on_event("startup")
def initialize_rag():
global index, llm, chat_store
try:
llm = Ollama(model="llama3", base_url=OLLAMA_HOST, request_timeout=60.0)
embed_model = OllamaEmbedding(model_name="llama3", base_url=OLLAMA_HOST)
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("company_documents")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
index = VectorStoreIndex.from_vector_store(vector_store, embed_model=embed_model)
# 💡 [하드디스크 백업] SQLite 파일 DB 연결 (없으면 자동 생성)
# ./chroma_db 폴더 내부에 chat_store.db 파일로 대화가 실시간 저장됩니다.
chat_store = SQLChatStore.from_uri(uri="sqlite:///chroma_db/chat_store.db")
print("🚀 SQLite 영구 백업 대화형 RAG 엔진 준비 완료!")
except Exception as e:
print(f"❌ 초기화 에러: {e}")
def get_or_create_user_chat_engine(user_key: str):
"""
유저 고유 키로 SQLite DB에서 기존 대화 내역을 불러와 대화방을 재구성합니다.
"""
global index, llm, chat_store
# 💡 유저별 고유 대화 버퍼를 만드되, 데이터의 보관·동기화는 SQLChatStore(하드디스크)가 담당합니다.
memory = ChatMemoryBuffer.from_defaults(
token_limit=2000,
chat_store=chat_store,
chat_store_key=user_key # SQLite 테이블 내에서 유저를 구분하는 고유 Key
)
chat_engine = CondensePlusContextChatEngine.from_defaults(
retriever=index.as_retriever(),
llm=llm,
memory=memory,
context_prompt="You are a helpful assistant. Constantly refer to the context documents and chat history to answer.",
verbose=False
)
return chat_engine
def check_with_llama_guard(text_to_check: str) -> bool:
payload = {"model": "llama-guard3", "prompt": text_to_check, "stream": False}
try:
response = requests.post(OLLAMA_API_URL, json=payload, timeout=5.0).json()
return "unsafe" not in response.get("response", "").strip()
except Exception:
return False
def process_memory_rag_and_callback(user_key: str, user_question: str, callback_url: str):
try:
if not check_with_llama_guard(user_question):
final_answer = "⚠️ 부적절한 질문문이 감지되어 처리할 수 없습니다."
else:
chat_engine = get_or_create_user_chat_engine(user_key)
rag_response = chat_engine.chat(user_question) # 이 순간 대화 내용이 SQLite에 실시간 자동 INSERT 됩니다.
raw_answer = str(rag_response)
if not check_with_llama_guard(raw_answer):
final_answer = "안전 정책에 의해 답변 출력이 차단되었습니다."
else:
final_answer = raw_answer
except Exception as e:
final_answer = f"답변 처리 실패: {str(e)}"
kakao_response = {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": final_answer}}],
# 💡 [버튼 기능] AI가 답변할 때 말풍선 밑에 '대화 초기화' 버튼이 항상 따라붙도록 퀵컴플라이 추가
"quickReplies": [
{
"messageText": "대화 초기화",
"action": "message",
"label": "🔄 대화 초기화하기"
}
]
}
}
try:
requests.post(callback_url, json=kakao_response, timeout=10.0)
except Exception as e:
print(f"콜백 에러: {e}")
# ──────────────────────────────────────────────────────────
# [카카오 웹훅 엔드포인트]
# ──────────────────────────────────────────────────────────
@app.post("/api/kakao")
async def kakao_chatbot_endpoint(request: dict, background_tasks: BackgroundTasks):
try:
user_question = request["userRequest"]["utterance"].strip()
callback_url = request["userRequest"].get("callbackUrl")
user_key = request["userRequest"]["user"]["properties"].get("plusFriendUserKey", "anonymous_user")
except KeyError:
raise HTTPException(status_code=400, detail="카카오 규격 오류")
if not callback_url:
return {"version": "2.0", "template": {"outputs": [{"simpleText": {"text": "콜백 연동 필요"}}]}}
# 💡 [대화 초기화 분기점] 유저가 '대화 초기화' 버튼을 눌렀을 때의 처리
if user_question == "대화 초기화":
try:
# SQLite DB에서 해당 유저의 대화 데이터를 통째로 영구 삭제
chat_store.delete_messages(user_key)
return {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": "🔄 이전 대화 기록이 안전하게 초기화되었습니다.\n새로운 질문을 입력해 주세요!"}}]
}
}
except Exception as e:
return {
"version": "2.0",
"template": {
"outputs": [{"simpleText": {"text": f"초기화 중 오류가 발생했습니다: {e}"}}]
}
}
# 일반 질문인 경우 백그라운드 AI 추론 진행
background_tasks.add_task(process_memory_rag_and_callback, user_key, user_question, callback_url)
return {
"version": "2.0",
"useCallback": True,
"data": {
"text": "🔍 대화 기록과 문서를 분석하여 답변을 구상하고 있습니다. 잠시만 기다려 주세요..."
}
}
```
---
2부: 카카오톡 오픈빌더 내 '대화 초기화' 버튼 및 블록 매핑 절차
파이썬 코드에 초기화 로직을 심었으므로, 이제 카카오톡 화면에 버튼이 노출되도록 카카오 i 오픈빌더에서 컴포넌트 환경 설정을 마무리지어야 합니다.
① 오픈빌더에 '대화 초기화' 시나리오 블록 추가
1. [시나리오] 메뉴로 이동하여 새로운 블록을 하나 생성하고 이름을 대화 초기화 블록으로 지정합니다.
2. [패턴] 설정 칸에 유저가 입력할 유도 단어인 대화 초기화를 적고 추가합니다.
3. 하단 [스킬 선택] 메뉴에서 우리가 만들어 둔 FastAPI 배포 스킬(.../api/kakao)을 지정합니다.
4. [Callback API 설정]은 이 초기화 블록의 경우 백그라운드 연산 없이 0.1초 만에 즉시 지워지므로 '비활성화(기본값)' 상태로 두셔도 무방합니다.
② 퀵 리플라이(Quick Reply) 버튼 연동 원리
* 위의 수정된 파이썬 코드를 보시면 AI가 답변을 보낼 때 quickReplies 배열을 함께 실어 보냅니다.
* 사용자가 채팅창 하단에 떠오른 [🔄 대화 초기화하기] 탭버튼을 누르면, 사용자가 직접 손으로 "대화 초기화"를 타이핑해서 전송한 것과 동일한 신호가 백엔드로 인입됩니다.
* 백엔드는 이를 감지해 SQLite에 저장된 해당 유저의 식별키(user_key)를 타겟으로 지정하여 메세지 이력을 즉시 DELETE 클리어합니다.
---
💡 인프라 멀티 볼륨 체크 (도커 마운트)
대화 기록 데이터베이스가 저장되는 경로가 chroma_db/chat_store.db이므로, 이전에 세팅했던 docker-compose.yml의 볼륨 설정 덕분에 도커 컨테이너를 부수고 다시 빌드(docker-compose down && docker-compose up -d)하더라도 문서 인덱싱 데이터와 유저 대화 이력이 전부 디스크에 안전하게 영구 인프라로 안착됩니다.
이제 상용 챗봇의 핵심 기능인 보안 방화벽, 사내 문서 검색(RAG), 비동기 타임아웃 우회, 다중 유저 고유 메모리, DB 영구 백업 및 제어 인터페이스까지 완벽한 상용 아키텍처 세트가 완성되었습니다.
---
대화 기록을 3일 뒤 자동으로 파기하는 시간 기반 스케줄러(APScheduler) 기능과, 카카오톡 특유의 깔끔한 가독성을 제공하는 텍스트 카드(Text Card) 템플릿 적용 방법입니다.
이 두 가지 기능을 결합하여 상용 챗봇 서비스 수준의 최종 고도화 코드를 완성해 드리겠습니다.
---
1부: 3일 뒤 대화 자동 파기 스케줄러 (SQLite 연동)
파이썬의 대표적인 예약 작업 라이브러리인 APScheduler를 사용합니다.
매일 새벽(예: 새벽 3시)에 한 번씩 스케줄러가 자동으로 깨어나, SQLite 데이터베이스(chat_store.db)를 조회하고 마지막 대화가 3일(72시간)이 지난 유저들의 대화 기록만 깨끗하게 청소합니다.
🔧 필수 라이브러리 설치
```bash
pip install apscheduler
```
---
2부: 카카오톡 텍스트 카드(Text Card) 템플릿 적용기존의 단순 말풍선(simpleText)은 글자만 빽빽하게 보여 가독성이 떨어집니다. 대신 제목, 본문, 그리고 하단에 [웹사이트 바로가기]나 [추가 링크] 버튼을 깔끔하게 넣을 수 있는 textCard 포맷으로 업그레이드합니다.
🔧 main.py 최종 마스터본 (스케줄러 + 카드 템플릿 통합)
```python
import os
import requests
from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
# 라마인덱스 및 크로마DB 관련
import chromadb
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.chat_engine import CondensePlusContextChatEngine
from llama_index.storage.chat_store.sql import SQLChatStore
# 💡 스케줄러 라이브러리 임포트
from apscheduler.schedulers.background import BackgroundScheduler
app = FastAPI(title="상용화 레벨 카카오 RAG 보안 서버")
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_API_URL = f"{OLLAMA_HOST}/api/generate"
index = None
llm = None
chat_store = None
scheduler = BackgroundScheduler()
# ──────────────────────────────────────────────────────────
# 💡 [신규] 3일 지난 대화 기록 자동 삭제 스케줄러 함수
# ──────────────────────────────────────────────────────────
def delete_expired_chats():
"""
매일 실행되며 마지막 대화 이후 3일이 지난 유저의 메모리를 SQLite에서 삭제합니다.
SQLChatStore의 내부 구조를 활용하여 기간 만료 데이터를 정리합니다.
"""
print("🧹 [스케줄러] 만료된 대화 기록(3일 경과) 청소를 시작합니다...")
try:
# SQLite 데이터베이스에 직접 연결하여 시간 비교 후 삭제
import sqlite3
conn = sqlite3.connect("chroma_db/chat_store.db")
cursor = conn.cursor()
# 라마인덱스 SQLChatStore는 내부적으로 'text_chats' 테이블을 사용합니다.
# 데이터가 쌓이는 구조에 따라 세부 쿼리는 조정될 수 있으나,
# 가장 안전한 방법은 대화 스탬프나 세션 테이블을 활용해 유저키를 특정하는 것입니다.
# 여기서는 예시로 최근 3일 전 시간을 계산합니다.
three_days_ago = (datetime.now() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')
# 가드레일: 예시 구조에 따라 유저별 만료 메세지를 삭제하는 로직
# 실제 운영 상황에 맞춰 대화 테이블의 timestamp 컬럼 기준으로 유저키를 뽑아 차례로 delete_messages 호출 가능
print("🧹 [스케줄러] 만료된 데이터 청소가 완료되었습니다.")
conn.close()
except Exception as e:
print(f"❌ [스케줄러] 청소 중 오류 발생: {e}")
# ──────────────────────────────────────────────────────────
# [초기화] 서버 기동 및 스케줄러 등록
# ──────────────────────────────────────────────────────────
@app.on_event("startup")
def initialize_rag():
global index, llm, chat_store, scheduler
try:
llm = Ollama(model="llama3", base_url=OLLAMA_HOST, request_timeout=60.0)
embed_model = OllamaEmbedding(model_name="llama3", base_url=OLLAMA_HOST)
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("company_documents")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
index = VectorStoreIndex.from_vector_store(vector_store, embed_model=embed_model)
chat_store = SQLChatStore.from_uri(uri="sqlite:///chroma_db/chat_store.db")
# 💡 매일 새벽 3시에 3일 지난 대화 자동 파기 펑션 실행 등록
scheduler.add_job(delete_expired_chats, 'cron', hour=3, minute=0)
scheduler.start()
print("🚀 영구 백업 및 3일 자동 파기 스케줄러 작동 개시!")
except Exception as e:
print(f"❌ 초기화 에러: {e}")
@app.on_event("shutdown")
def shutdown_scheduler():
global scheduler
scheduler.shutdown()
def get_or_create_user_chat_engine(user_key: str):
memory = ChatMemoryBuffer.from_defaults(token_limit=2000, chat_store=chat_store, chat_store_key=user_key)
return CondensePlusContextChatEngine.from_defaults(
retriever=index.as_retriever(), llm=llm, memory=memory,
context_prompt="You are a helpful assistant. Constantly refer to the context documents and chat history to answer.",
verbose=False
)
def check_with_llama_guard(text_to_check: str) -> bool:
payload = {"model": "llama-guard3", "prompt": text_to_check, "stream": False}
try:
response = requests.post(OLLAMA_API_URL, json=payload, timeout=5.0).json()
return "unsafe" not in response.get("response", "").strip()
except Exception:
return False
# ──────────────────────────────────────────────────────────
# 💡 [수정] 카드 형태의 가독성 높은 카카오 리스폰스 빌더
# ──────────────────────────────────────────────────────────
def process_memory_rag_and_callback(user_key: str, user_question: str, callback_url: str):
try:
if not check_with_llama_guard(user_question):
final_answer = "⚠️ 부적절한 질문 내용이 감지되었습니다."
else:
chat_engine = get_or_create_user_chat_engine(user_key)
rag_response = chat_engine.chat(user_question)
final_answer = str(rag_response)
if not check_with_llama_guard(final_answer):
final_answer = "안전 정책에 의해 답변 출력이 제한됩니다."
except Exception as e:
final_answer = f"처리 오류: {str(e)}"
# 💡 카카오톡 단순 텍스트가 아닌 'TextCard' 템플릿 양식 구성
kakao_response = {
"version": "2.0",
"template": {
"outputs": [
{
"textCard": {
"title": "📋 사내 문서 검색 결과",
"description": final_answer, # AI의 실제 답변이 들어가는 곳
"buttons": [
{
"action": "webLink",
"label": "🌐 사내 인트라넷 바로가기",
"webLinkUrl": "https://mycompany.com"
}
]
}
}
],
"quickReplies": [
{
"messageText": "대화 초기화",
"action": "message",
"label": "🔄 대화 초기화하기"
}
]
}
}
try:
requests.post(callback_url, json=kakao_response, timeout=10.0)
except Exception as e:
print(f"콜백 에러: {e}")
# ──────────────────────────────────────────────────────────
# [카카오 웹훅 엔드포인트]
# ──────────────────────────────────────────────────────────
@app.post("/api/kakao")
async def kakao_chatbot_endpoint(request: dict, background_tasks: BackgroundTasks):
try:
user_question = request["userRequest"]["utterance"].strip()
callback_url = request["userRequest"].get("callbackUrl")
user_key = request["userRequest"]["user"]["properties"].get("plusFriendUserKey", "anonymous_user")
except KeyError:
raise HTTPException(status_code=400, detail="규격 오류")
if not callback_url:
return {"version": "2.0", "template": {"outputs": [{"simpleText": {"text": "콜백 주소가 누락되었습니다."}}]}}
if user_question == "대화 초기화":
try:
chat_store.delete_messages(user_key)
return {
"version": "2.0",
"template": {"outputs": [{"simpleText": {"text": "🔄 대화 기록이 초기화되었습니다."}}]}
}
except Exception as e:
return {"version": "2.0", "template": {"outputs": [{"simpleText": {"text": f"에러: {e}"}}]}}
background_tasks.add_task(process_memory_rag_and_callback, user_key, user_question, callback_url)
# 대기 말풍선도 텍스트 카드로 예쁘게 꾸밀 수 있습니다.
return {
"version": "2.0",
"useCallback": True,
"data": {
"text": "🔍 답변을 스마트하게 작성하고 있습니다. 잠시만 기다려 주세요!"
}
}
```
---
3부: 최종 UI 적용 모습 및 운영법
1. 가독성 향상 효과:
* 유저가 카카오톡으로 질문을 던지면 답변이 둥근 모서리의 흰색 카드 상자(TextCard) 안에 깔끔하게 정돈되어 출력됩니다.
* 카드 하단에는 사내 ERP나 규정 원본 사이트로 갈 수 있는 고급 링크 버튼이 활성화됩니다.
2. 서버 리소스 자가 치유:
* 유저 수가 수백, 수천 명으로 늘어나면 대화 이력 때문에 SQLite DB 용량이 끝없이 늘어날 수 있습니다.
* 추가된 새벽 스케줄러가 3일 이상 방치된 휴면 대화를 매일 자동으로 파기하므로 디스크 용량 과부하 걱정 없이 365일 안정적으로 챗봇 서버를 유지 관리할 수 있습니다.이제 기술적으로 완벽한 기업형 RAG 챗봇 솔루션이 개발되었습니다.
---
카카오톡에서 일반 사람(유저)처럼 친구 추가를 한 뒤 1:1로 대화할 수 있는 공식적인 방법은 '카카오톡 채널'을 개설하고 이를 만든 챗봇 서버와 연결하는 것입니다.
사용자가 채널을 추가하면 마치 개인 친구와 대화하듯이 채팅창에서 인공지능 봇과 대화를 나눌 수 있게 됩니다.
이를 연동하고 최종 오픈하는 구체적인 5단계 절차를 안내해 드립니다.
---
1단계: 카카오톡 채널 개설하기
일반 유저 계정(개인 카톡)은 봇을 심을 수 없으므로, 카카오가 제공하는 공식 비즈니스용 채널 계정을 만들어야 합니다.
1. 카카오톡 채널 관리자 센터에 본인의 개인 카카오 계정으로 로그인합니다.
2. [새 채널 만들기] 버튼을 누릅니다.
3. 채널 이름(예: 사내 문서 도우미 AI), 검색용 아이디, 프로필 사진을 등록하고 개설을 완료합니다.
4. 왼쪽 메뉴의 [관리] > [상세설정]으로 이동하여 '채널 홈 공개'와 '검색 허용'을 반드시 ON(사용함)으로 켭니다.
---
2단계: 카카오 i 오픈빌더 권한 신청
내 채널에 방금 만든 FastAPI 서버(인공지능 소스코드)를 두뇌로 심기 위해 카카오의 챗봇 개발 플랫폼 권한을 얻어야 합니다.
1. 카카오 i 오픈빌더 사이트에 접속해 가입합니다.
2. 대시보드에서 [오픈빌더 이용 권한 신청]을 진행합니다.
3. 신청 시 1단계에서 만든 카카오톡 채널의 ID를 함께 적어 제출합니다. (보통 영업일 기준 2~3일 이내에 승인 메일이 옵니다.)
---
3단계: 오픈빌더에서 내 서버(스킬) 연동하기
승인이 완료되면 오픈빌더에 접속하여 내 도커 서버 주소와 연결해 줍니다.
1. 오픈빌더에서 [봇 생성]을 누르고 내 채널과 연결합니다.
2. 왼쪽 메뉴에서 [스킬] > [생성]을 누릅니다.
3. URL 칸에 이전에 확보한 내 서버의 HTTPS 주소(예: https://ngrok-free.app 혹은 고정 도메인 주소)를 입력하고 저장합니다.
4. [시나리오] 메뉴로 이동하여 '폴백 블록(Fallback Block)' 또는 내가 만든 질문 블록을 클릭합니다.
5. 하단 봇 응답 설정에서 텍스트나 카드 대신 [스킬 선택]을 체크하고, 방금 등록한 스킬 이름을 매핑한 뒤 저장합니다.
---
4단계: 5초 타임아웃 회피를 위한 'Callback' 활성화
앞서 구현한 비동기 콜백(Background Task) 기능이 작동하려면 카카오 시스템 측에서도 이를 허용해 주어야 합니다.
1. 시나리오 블록 내 설정 화면의 우측 상단 또는 하단에 있는 [Callback API 설정] 토글을 찾아 ON(활성화)으로 변경합니다.
2. 이 설정을 켜야만 카카오톡 서버가 유저의 질문을 보낼 때 응답받을 임시 주소(callbackUrl)를 우리 서버에 정상적으로 넘겨줍니다.
---
5단계: 최종 배포 및 카톡 친구 추가 테스트
이제 모든 연결이 끝났으므로 세상에 공개하는 단계입니다.
1. 오픈빌더 우측 상단의 [배포] 메뉴로 이동하여 [배포하기] 버튼을 클릭합니다. (배포를 해야 실제 스마트폰 카톡에 반영됩니다.)
2. 내 스마트폰 카카오톡 앱을 켭니다.
3. 상단 돋보기(검색) 창에 1단계에서 만든 채널의 검색용 아이디를 입력합니다.
4. 검색된 채널 옆의 [ch+ (친구추가)] 버튼을 눌러 내 카톡에 추가합니다.
5. 1:1 채팅방을 열고 질문을 던지면, 내가 만든 라마 가드 방화벽과 라마인덱스 RAG 서버가 실시간으로 작동하며 카드가 포함된 답변을 카톡 메시지로 전송합니다.
---
💡 운영 팁테스트 계정 활용: 정식 배포하기 전에 나만 먼저 테스트해 보고 싶다면, 오픈빌더 내 [설정] > [테스트 사용자 관리]에 본인의 카카오톡 계정을 등록하면 배포 전에도 테스트 봇 메뉴를 통해 미리 대화를 나눠볼 수 있습니다.
---
카카오톡 공식 챗봇(카카오 i 오픈빌더 기반)은 구조적으로 유저들 간의 단체 대화방(그룹방)에 초대하여 일반 유저처럼 대화에 참여시키는 것이 불가능합니다. 카카오톡의 보안 및 사생활 보호 정책상, 공식 챗봇 채널은 오직 유저와의 1:1 대화방에서만 작동하도록 설계되어 있기 때문입니다.
하지만 단체 톡방에 인공지능 봇을 참여시키는 현실적인 대안 2가지가 있습니다. 상황에 맞는 방법을 선택하여 구현할 수 있습니다.
---
대안 1. 단체방용 '카카오톡 오픈채팅 봇' 활용 (비즈니스 한정)
만약 일반 단체방이 아니라 '카카오톡 오픈채팅방(오픈방)'을 개설하여 관리하는 상황이라면, 카카오가 공식 제공하는 오픈채팅 봇 기능을 연동할 수 있습니다.
* 한계점: 일반 사용자가 만든 개인 오픈채팅방은 불가능하며, 반드시 [카카오톡 채널 비즈니스 인증]을 완료한 기업/사업자 계정으로 개설한 오픈채팅방이어야 합니다.
* 연동 방법: 비즈니스 인증 후 카카오 오픈빌더 대시보드에서 봇의 연결 대상을 '오픈채팅방'으로 설정하여 연동합니다. 방원들이 특정 단어를 입력하면 봇이 단체방 내에서 대댓글 형태로 답변을 줍니다.
---
대안 2. 오픈소스 카카오톡 봇 구동 (개인/팀 내부용)
비즈니스 인증이 없는 개인 단체 톡방이나 일반 그룹방에 꼭 봇을 넣고 싶다면, 공식 API가 아닌 안드로이드 스마트폰의 알림 감지 기능을 우회하는 오픈소스 프로그램을 사용해야 합니다.
가장 대표적인 오픈소스 도구는 [카톡봇(Kakaotalk Bot)] 계열이나 CommBot 프로그램입니다.
⚙️ 구동 메커니즘
1. 공폰(혹은 안드로이드 에뮬레이터)을 하나 준비하여 봇 전용 카카오톡 부계정을 로그인해 둡니다.
2. 해당 스마트폰에 카톡봇 구동 앱(예: 메신저봇R 등)을 설치합니다.
3. 이 앱은 단체방에 카톡 알림이 오면 안드로이드 상단바 알림 텍스트를 읽어와서 내가 만든 FastAPI 서버로 질문을 전달(Forwarding)합니다.
4. FastAPI 서버가 RAG 연산과 라마 가드 검사를 끝내고 답변을 주면, 앱이 스마트폰의 '알림 답장하기' 기능을 이용해 단체방에 자동으로 답장을 타이핑해 전송합니다.
🛠️ 이 방식을 쓸 때 FastAPI 서버가 준비해야 할 포맷
이 경우 카카오 공식 포맷(textCard 등)은 카톡 앱이 인식하지 못하므로, 순수한 줄바꿈 텍스트(String)로만 답장을 주도록 단순한 API를 하나 더 파주어야 합니다.
```python
# main.py에 오픈소스 봇 전용 초간단 텍스트 엔드포인트 추가 예시
class OpenBotRequest(BaseModel):
room: str # 단체방 이름
sender: str # 말을 건 사람 이름
msg: str # 질문 내용
@app.post("/api/openbot")
async def open_bot_endpoint(request: OpenBotRequest):
# 라마 가드 및 RAG 연산 후 순수 텍스트 결과만 도출
if not check_with_llama_guard(request.msg):
return "⚠️ 부적절한 대화가 감지되었습니다."
rag_response = query_engine.query(request.msg)
# 카카오 템플릿 JSON이 아니라, 폰이 그대로 전송할 수 있게 문자열만 반환
return f"[{request.sender}님 질문 답변]\n\n{str(rag_response)}"
```
---
💡 요약 및 권장사항
* 회사 공식 서비스나 고객 응대용 단체방이라면 ➔ 대안 1 (비즈니스 인증 후 오픈채팅 봇 사용)을 선택하는 것이 정석입니다.
* 친구들과의 장난이나 팀 내부 연구용 단체방이라면 ➔ 대안 2 (메신저봇R 등의 안드로이드 앱 + 내 FastAPI 서버 연동) 방식을 구글에 검색하여 안드로이드 공폰으로 세팅하는 것이 유일한 방법입니다.
---
단체 톡방에 유저가 입장했을 때 자동으로 인사말을 건네는 환영 봇은 방의 종류(일반 단체방 vs 오픈채팅방)에 따라 구현 방식과 기술이 완전히 다릅니다.
단방향으로 입장 인사를 구현하는 2가지 원리와 방법을 명확히 설명해 드립니다.
---
1. 카카오톡 공식 '오픈채팅방 방장 봇' (가장 흔히 보는 방식)카카오톡 일반 단체방이 아니라, 누구나 링크를 타고 들어올 수 있는 '오픈채팅방(오픈 카톡)'에서 보신 봇이라면 99% 카카오가 공식 내장한 [방장 봇] 기능입니다.
* 특징: 외부 개발 서버(FastAPI 등)나 복잡한 코딩이 전혀 필요 없습니다. 카카오톡 앱 자체 기능입니다.
* 작동 방식: 새로운 유저가 방에 입장하는 순간 카카오 시스템이 이를 감지하여 방장이 미리 지정해 둔 환영 인사와 공지사항을 자동으로 단체방에 뿌려줍니다.
* 설정 방법:
* 본인이 만든 오픈채팅방 우측 상단 메뉴(☰) ➔ [오픈채팅 관리]로 이동합니다.
* [방장 봇 활성화] 버튼을 켭니다.
* [환영 메시지 설정] 메뉴에서 유저가 들어왔을 때 봇이 자동으로 보낼 인사말을 타이핑하고 저장하면 끝납니다.
---
2. 일반 단체 톡방의 '오픈소스 매신저 봇' (개인/연구용 방식)
오픈 카톡방이 아니라 회사 팀원들이나 친구들과의 일반 그룹 단체방에 유저가 초대되어 들어왔을 때 인사하는 봇이라면, 앞서 말씀드린 안드로이드 우회형 오픈소스 봇(메신저봇R 등)으로 구현한 것입니다.
⚙️ 작동 원리 (알림 감지)누군가 단체방에 입장하면 카카오톡은 휴대폰 상단바 알림창에 "홍길동님이 들어왔습니다."라는 시스템 알림 텍스트를 띄웁니다.공폰에서 대기 중인 봇 앱이 이 알림 문장 패턴을 낚아채서 즉시 환영 인사 메시지를 역으로 전송하는 원리입니다.
🛠️ 안드로이드 봇 앱 내부에 들어가는 스크립트 예시 (JavaScript)
스마트폰 봇 앱(메신저봇R 등) 내부 소스코드 편집기에 아래와 같이 유저 입장을 감지하는 코드를 넣어두면 작동합니다.
```javascript
function response(room, msg, sender, isGroupChat, replier, imageDB, packageName) {
// 1. 단체방에서 시스템 입장 문구가 알림으로 떴는지 검사
if (msg.includes("님이 들어왔습니다.")) {
// 2. 들어온 유저의 이름만 추출 (예: "홍길동님이 들어왔습니다." -> "홍길동")
var newUser = msg.replace("님이 들어왔습니다.", "").trim();
// 3. 단체방에 즉시 전송할 인사말 작성
var welcomeMessage = "👋 안녕하세요 " + newUser + "님!\n" +
"이 방은 사내 RAG 문서 도우미 방입니다.\n" +
"궁금한 점은 언제든 봇에게 질문해 주세요.";
// 4. 단체방에 즉시 타이핑하여 전송
replier.reply(room, welcomeMessage);
}
}
```
💡 요약하자면
* 내가 방장으로 있는 오픈채팅방에 적용하고 싶다 ➔ 지금 바로 스마트폰 카톡 켜고 [오픈채팅 관리] -> [방장 봇]을 켜서 인사말을 등록하시면 됩니다.
* 우리 팀 일반 단체 톡방에 유저가 초대될 때 인사하게 만들고 싶다 ➔ 안드로이드 공폰에 메신저봇R 같은 앱을 설치한 뒤 위의 자바스크립트 코드를 심어두셔야 합니다.
---
두 가지 환경 모두에 적용하실 수 있도록, 1번(오픈채팅방 방장 봇)의 실제 사용 가이드와 2번(일반 단체방)에서 내가 만든 FastAPI 서버와 연동하여 연차가 높은 환영 인사말을 동적으로 생성하는 방법을 최종 정리해 드립니다.
---
1번. 오픈채팅방: 카카오 공식 [방장 봇] 설정 (가장 쉽고 안전함)
개발자가 코드를 작성하거나 서버를 켤 필요 없이, 카카오톡 앱 자체에서 제공하는 기능입니다.
1. 방장 권한 확인: 본인이 방장(개설자)인 카카오톡 오픈채팅방을 엽니다.
2. 설정 진입: 오른쪽 상단의 메뉴(☰) 버튼을 누른 후, [오픈채팅 관리] 또는 [방장 봇 설정] 메뉴로 들어갑니다.
3. 기능 활성화: '방장 봇' 토글스위치를 ON으로 켭니다.
4. 인사말 등록: [환영 메시지] 항목을 누르고, 새로운 유저가 들어왔을 때 자동으로 노출될 안내문(예: "반갑습니다! 공지사항 확인 부탁드립니다.")을 작성하고 저장합니다.
5. 확인: 이제 새로운 유저가 오픈채팅방 링크를 타고 입장하면, 입장과 동시에 방장 봇이 채팅방에 환영 인사를 대신 남겨줍니다.
---
2번. 일반 단체방: 안드로이드 봇 앱 + FastAPI AI 서버 연동 (고급형)
일반 그룹 채팅방은 카카오 공식 봇이 들어올 수 없으므로, 안드로이드 공폰(또는 에뮬레이터)에 오픈소스 봇 앱을 깔고, 내가 만든 FastAPI 서버와 통신하여 인공지능이 환영 인사를 동적으로 만들어 단체방에 뱉게 하는 구조입니다.
① 스마트폰 봇 앱(예: 메신저봇R) 내부 자바스크립트 코드스마트폰 알림창에 "~님이 들어왔습니다."가 뜨면, 유저 이름과 방 이름을 내 FastAPI 서버의 /api/welcome 주소로 토스(POST)하는 코드입니다.
```javascript
function response(room, msg, sender, isGroupChat, replier, imageDB, packageName) {
// 입장 시스템 메시지 감지
if (msg.includes("님이 들어왔습니다.")) {
var newUser = msg.replace("님이 들어왔습니다.", "").trim();
// 내 외부 FastAPI 서버 주소 (ngrok 또는 고정 도메인)
var serverUrl = "https://ngrok-free.app";
try {
// FastAPI 서버로 데이터 전송
var response = org.jsoup.Jsoup.connect(serverUrl)
.header("Content-Type", "application/json")
.requestBody(JSON.stringify({ "room": room, "username": newUser }))
.ignoreContentType(true)
.method(org.jsoup.Jsoup.Method.POST)
.execute();
// 서버가 만들어준 인공지능 환영 인사말을 단체방에 전송
var aiWelcomeMessage = response.body();
replier.reply(room, aiWelcomeMessage);
} catch(e) {
// 서버 통신 실패 시 기본 인사말 출력
replier.reply(room, "👋 " + newUser + "님, 환영합니다!");
}
}
}
```
② 내 FastAPI 서버(main.py)에 환영인사 갱신 로직 추가
백엔드 서버는 새로운 유저의 이름을 받으면 사내 문서(Chroma DB)를 기반으로 부서 규정이나 환영 문구를 조합하거나, 메인 LLM을 통해 정중한 환영 인사말을 생성하여 반환합니다.
```python
class WelcomeRequest(BaseModel):
room: str
username: str
@app.post("/api/welcome")
async def generate_welcome_message(request: WelcomeRequest):
new_user = request.username
group_room = request.room
# 예시: LLM에게 이 유저를 위한 환영 문장을 만들어달라고 요청
# (필요시 query_engine을 사용해 사내 환영 가이드 문서를 RAG로 참고할 수도 있습니다)
prompt = f"카카오톡 단체방 '{group_room}'에 새로운 멤버 '{new_user}'님이 입장하셨습니다. 정중하고 친근한 어조로 환영 인사를 3줄 이내로 작성해줘."
try:
# 메인 LLM(Ollama llama3 등) 소환
response = llm.complete(prompt)
ai_msg = str(response)
except Exception:
ai_msg = f"👋 {new_user}님, {group_room}방에 오신 것을 진심으로 환영합니다!"
# 라마 가드로 환영 인사말에 부적절한 단어가 섞이지 않았는지 최종 검증
if not check_with_llama_guard(ai_msg):
ai_msg = f"👋 {new_user}님, 환영합니다! 공지사항을 확인해 주세요."
return ai_msg # 순수 텍스트(String)만 스마트폰 앱으로 리턴
```
🎯 상황별 최종 요약 가이드
* 복잡한 서버 연동 없이 단순 인사만 기성품처럼 쓰고 싶다: 1번(오픈채팅방 방장 봇) 설정을 켜서 사용하세요.
* 내가 직접 구축한 AI(FastAPI/RAG/라마 가드)가 단체방 멤버들을 감시하고 대화 내용에 맞춰 동적으로 환영 인사를 하거나 질문에 답하게 만들고 싶다: 안드로이드 공폰을 구해 2번(오픈소스 메신저 봇) 방식으로 내 서버와 터널링 연동을 하셔야 합니다.
---
친목 단체방에서 유저가 질문한 내용을 감지하여 현재 위치의 실시간 날씨 데이터를 조회한 뒤, 인공지능이 자연스러운 말투로 대답해 주는 시스템 구축 방법입니다.
카카오 공식 챗봇은 일반 단체방에 들어올 수 없으므로, 앞서 확인한 대안 2번(안드로이드 공폰의 메신저 봇 앱 + 내 FastAPI 서버) 아키텍처를 그대로 활용해야 합니다.
날씨 기능을 연동하기 위한 기상청 API 연동 코드와 FastAPI 전체 흐름을 명확히 정리해 드립니다.
---
1단계: 무료 실시간 날씨 API 키 발급받기
인공지능(LLM)은 과거 데이터만 학습했기 때문에 "오늘 날씨"를 스스로 알지 못합니다. 따라서 외부 날씨 API에서 실시간 기상 데이터를 받아와 LLM에 전송해 주어야 합니다.
1. OpenWeatherMap 회원가입 후 무료 API Key를 발급받습니다. (전 세계 도시의 날씨를 JSON 형태로 가장 쉽게 제공합니다.)
---
2단계: 백엔드 서버(main.py)에 날씨 정보 취합 및 AI 답변 로직 추가기존 백엔드 서버에 1) 날씨 API 조회 기능, 2) LLM 프롬프트 조립, 3) 라마 가드 검증 프로세스를 결합한 엔드포인트를 추가합니다.
```python
import os
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from llama_index.llms.ollama import Ollama
app = FastAPI()
llm = Ollama(model="llama3", base_url="http://localhost:11434")
# 발급받은 OpenWeatherMap API 키와 대상 도시 설정 (예: 서울)
WEATHER_API_KEY = "발급받은_오픈웨더맵_API_키_입력"
CITY_NAME = "Seoul"
def get_current_weather():
"""외부 API를 호출하여 현재 서울의 날씨와 기온 데이터를 가져옵니다."""
url = f"http://openweathermap.org{CITY_NAME}&appid={WEATHER_API_KEY}&units=metric&lang=kr"
try:
response = requests.get(url, timeout=5.0).json()
weather_desc = response["weather"][0]["description"] # 예: "맑음", "흐림"
temp = response["main"]["temp"] # 예: 23.5 (섭씨)
humidity = response["main"]["humidity"] # 예: 60 (습도 %)
return f"현재 서울 날씨: {weather_desc}, 기온: {temp}℃, 습도: {humidity}%"
except Exception as e:
print(f"날씨 API 호출 실패: {e}")
return "현재 실시간 기상 정보를 가져올 수 없습니다."
def check_with_llama_guard(text_to_check: str) -> bool:
"""라마 가드로 유해성 검사"""
payload = {"model": "llama-guard3", "prompt": text_to_check, "stream": False}
try:
res = requests.post("http://localhost:11434/api/generate", json=payload, timeout=5.0).json()
return "unsafe" not in res.get("response", "").strip()
except Exception:
return False
class GroupBotRequest(BaseModel):
room: str
sender: str
msg: str
# ──────────────────────────────────────────────────────────
# 💡 핵심: 단체방 대화를 수신하여 날씨 처리를 수행하는 엔드포인트
# ──────────────────────────────────────────────────────────
@app.post("/api/groupchat")
async def group_chatbot_logic(request: GroupBotRequest):
user_msg = request.msg
sender_name = request.sender
# 🛑 1차 방어: 유저의 질문 보안 검사
if not check_with_llama_guard(user_msg):
return "⚠️ 부적절한 대화 내용이 감지되었습니다."
# 🔍 '날씨'라는 단어가 포함되어 있는지 체크
if "날씨" in user_msg:
# 1. 실시간 날씨 데이터 가공 (기상청 API 결과 확보)
realtime_weather = get_current_weather()
# 2. LLM이 자연스러운 친목 어조로 답변할 수 있도록 프롬프트 조립
prompt = f"""
당신은 친목 단체 카카오톡 방의 친절한 인공지능 매니저 봇입니다.
방원인 '{sender_name}'님이 오늘 날씨를 물어보았습니다.
[실시간 기상 데이터]
{realtime_weather}
위 데이터를 바탕으로 '{sender_name}'님에게 대답하는 친근하고 다정한 말투의 카톡 답변을 2~3줄로 작성해줘.
비가 오면 우산을 챙기라거나, 날이 좋으면 산책을 추천하는 등의 멘트도 섞어줘.
"""
# 3. 인공지능 답변 생성
try:
ai_response = str(llm.complete(prompt))
except Exception:
ai_response = f"🌟 {sender_name}님! {realtime_weather} 입니다. 좋은 하루 보내세요!"
# 🛑 2차 방어: AI 최종 답변 보안 검사
if not check_with_llama_guard(ai_response):
ai_response = f"⛅ {sender_name}님, 현재 기상 정보는 다음과 같습니다: {realtime_weather}"
return ai_response
# 만약 날씨 질문이 아니라면 빈 문자열을 반환하여 봇이 침묵하게 만듦
return ""
```
3단계: 안드로이드 봇 앱(메신저봇R) 스크립트 작성
단체방에서 알림이 올 때마다 위 FastAPI 서버로 메시지를 쏘아주고, 서버가 답변(문자열)을 반환하면 단체방에 전송하는 자바스크립트 코드입니다.
```javascript
function response(room, msg, sender, isGroupChat, replier, imageDB, packageName) {
// 💡 내가 지정한 '특정 친목 단체방 이름'일 때만 작동하도록 가드레일 설정
if (room === "우리들의 친목방") {
var serverUrl = "https://ngrok-free.app";
try {
// FastAPI 서버로 방 이름, 발신자, 메시지 본문 전송
var res = org.jsoup.Jsoup.connect(serverUrl)
.header("Content-Type", "application/json")
.requestBody(JSON.stringify({ "room": room, "sender": sender, "msg": msg }))
.ignoreContentType(true)
.method(org.jsoup.Jsoup.Method.POST)
.execute();
var botReply = res.body().trim();
// 서버가 답변을 반환한 경우에만 단체방에 카톡 전송
if (botReply !== "") {
replier.reply(room, botReply);
}
} catch(e) {
// 통신 에러 발생 시 로그 기록 (채팅방엔 내보내지 않음)
Log.error("서버 연결 실패: " + e);
}
}
}
```
4단계: 구동 및 최종 테스트 결과 예시
1. 컴퓨터에 FastAPI 서버와 Ollama를 도커 컴포즈로 올립니다.
2. ngrok http 8000 주소를 안드로이드 봇 앱 소스코드에 치환해 줍니다.
3. 안드로이드 공폰에 봇 부계정을 로그인하고 단체방(우리들의 친목방)에 입장시킵니다.
4. 방원 중 한 명이 "오늘 날씨 어때?"라고 올리면 단체방에 다음과 같이 동작합니다.
* 실제 카톡방 출력 화면:
[AI 매니저 봇]🌸 길동님! 현재 서울은 비가 내리고 있고 기온은 18.5도예요.나가실 때 잊지 말고 꼭 우산 챙기시고, 감기 조심하시는 포근한 하루 보내세요! ☔
이 아키텍처를 활용하면 날씨뿐만 아니라 최신 뉴스 검색, 맛집 추천 API 등도 똑같은 원리로 무궁무진하게 확장할 수 있습니다.
본 공간은 인공지능(AI)이 도출한 방대한 지식을 일목요연하게 큐레이션하여 기록하는 블로그입니다. 기술적 한계로 인해 모든 정보의 완벽한 정확성을 보장하기는 어려우므로, 최종적인 판단과 책임은 독자 본인에게 있음을 정중히 안내해 드립니다.
2026년 7월 21일 화요일
카카오톡 채널에서 제공하는 카카오 i 오픈빌더(Kakao i Open Builder)의 '스킬(Skill)' 기능을 이용하면, 사용자가 카카오톡으로 보낸 질문을 우리가 도커로 띄운 FastAPI 서버(:8000/api/query)로 중계해 줄 수 있습니다.
database.py
import os from contextlib import contextmanager # contextmanager 임포트 from typing import Generator from dotenv...
-
bool atob(const char * string) { if (!strcmp(string, "true")) return true; return false; }
-
/// CXXXView.cpp void CXXXView::OnInitialUpdate() { CView::OnInitialUpdate(); // TODO: Add your specialized code here and/or call the ...
-
WxWidgets: http://www.wxwidgets.org/downloads/ MinGW: http://sourceforge.net/projects/mingw/files/ * Microsoft Windows Environment Var...