📑 이 챕터에서 다룰 내용
4편 (Node.js + Discord)과 동일한 메타 원칙에 Python + Telegram을 결합합니다. 한 서버에서 두 봇을 동시에 운영할 수 있어요.
| 항목 | 내용 |
|---|---|
| 예상 시간 | 1~2시간 |
| 완성 결과 | Telegram 채팅에서 /ask → Claude API → 답변 |
- 동일 시리즈 메타 원칙 (⚖️ Position C, LogOnTable, 다층 보호)
- 동일 흐름 (질문 → API → 응답 + footer)
- 다른 SDK (discord.js vs python-telegram-bot)
- 한 서버에 두 봇 동시 운영 가능
Python 패키지 관리는 uv를 권장합니다. Astral이 만든 2024년~ 표준 도구로, pip보다 훨씬 빠릅니다.
# uv 설치 (Astral, 2024년~ 표준) curl -LsSf https://astral.sh/uv/install.sh | sh source ~/.bashrc # 확인 uv --version # Python 3.12 설치 uv python install 3.12 # 또는 시스템 Python (Ubuntu 24.04 기본) sudo apt install -y python3 python3-pip python3-venv python3 --version # 3.12.x
Telegram 봇은 @BotFather를 통해 만듭니다. Telegram 앱에서 바로 진행할 수 있어요.
[BotFather] 1. Telegram 앱에서 @BotFather 검색 + 채팅 시작 2. /newbot 입력 3. 봇 이름 입력 (예: "TSV Assistant") 4. 봇 username 입력 (예: "tsv_assistant_bot", "_bot" 끝 의무) 5. ★ HTTP API 토큰 받기 (안전 보관) 예: 1234567890:ABCdef... [★ 함정 회피] - 토큰 노출 시 즉시 /revoke 후 재발급 - /setcommands 로 명령어 등록 권장: · ask - Claude 에게 질문 · help - 도움말
토큰이 노출되면 즉시 BotFather에서 /revoke로 무효화하고 재발급받으세요.
절대 코드에 직접 쓰거나 Git에 push하지 마세요.
Discord 봇과 별도 디렉토리를 만들어 분리합니다. uv로 가상환경을 생성하고 패키지를 설치합니다.
# 작업 디렉토리 mkdir ~/tsv-tg-bot cd ~/tsv-tg-bot # 가상환경 (uv) uv venv source .venv/bin/activate # 패키지 설치 uv pip install python-telegram-bot anthropic python-dotenv # 또는 requirements.txt 방식 cat > requirements.txt << 'EOF' python-telegram-bot==21.6 anthropic==0.40.0 python-dotenv==1.0.1 EOF uv pip install -r requirements.txt
디렉토리 구조
~/tsv-tg-bot/ ├── .env ├── .gitignore ├── requirements.txt ├── src/ │ ├── bot.py │ ├── claude.py │ ├── logger.py │ └── messages.py └── logs/
API 키와 토큰은 .env에 보관하고 반드시 .gitignore로 제외합니다.
# .env cat > .env << 'EOF' TELEGRAM_TOKEN=your_telegram_bot_token_here ANTHROPIC_API_KEY=your_anthropic_api_key_here ANTHROPIC_MODEL=claude-sonnet-4-6 LOG_DIR=./logs EOF # .gitignore cat > .gitignore << 'EOF' .venv/ __pycache__/ *.pyc .env logs/ *.log .DS_Store EOF chmod 600 .env
4편 (TypeScript)과 동일한 내용을 Python으로 구현합니다. 시리즈 메타 원칙을 플랫폼과 언어에 관계없이 일관되게 적용합니다.
# src/messages.py
"""
⚖️ Position C — 시리즈 메타 원칙 일관
"""
POSITION_C_FOOTER = """
━━━━━━━━━━━━━━━━━━━━━━━━━━━
이 봇은 분석 콘텐츠 보조 도구이며,
법률·의료·금융 등 전문 조언을 제공하지 않습니다.
중요한 결정은 전문가와 상의하시기 바랍니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━
""".strip()
SYSTEM_PROMPT = """
당신은 TSV 운영자의 보조 도구입니다.
[원칙]
- 사실 단일 출처 (확실하지 않은 사실 추측 X)
- 베팅·픽 추천 표현 X
- 법률·의료·금융 단정 표현 X
- 사용자 질문에 정확하고 간결하게 답변
[톤]
- 한국어 기본
- 전문적이고 친근함
[제약]
- 답변 길이 1500자 이내 (Telegram 4096자 한도 안전)
- 코드 블록 사용 권장
""".strip()
ERROR_MESSAGES = {
"rate_limit": "⏰ 잠시 후 다시 시도해주세요. (rate limit)",
"api_error": "⚠️ 일시적 오류가 발생했습니다.",
"too_long": "📝 질문이 너무 깁니다. 1000자 이내로 줄여주세요.",
"no_question": "질문을 입력해주세요. 예: /ask 안녕",
}
Python용 Anthropic SDK wrapper입니다. TypeScript 버전과 동일한 에러 처리 구조를 Python 커스텀 예외로 구현합니다.
# src/claude.py
import os
from anthropic import Anthropic, APIStatusError, APITimeoutError, APIConnectionError
from messages import SYSTEM_PROMPT
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-6")
class TooLongError(Exception):
pass
class RateLimitError(Exception):
pass
class ApiError(Exception):
pass
async def ask_claude(question: str) -> str:
if len(question) > 1000:
raise TooLongError()
try:
response = client.messages.create(
model=MODEL,
max_tokens=1500,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": question}],
)
# text 블록만 추출
for block in response.content:
if block.type == "text":
return block.text
return "⚠️ 답변을 생성할 수 없습니다."
except APIStatusError as e:
if e.status_code == 429:
raise RateLimitError() from e
if e.status_code >= 500:
raise ApiError() from e
raise
except (APITimeoutError, APIConnectionError) as e:
raise ApiError() from e
TypeScript 버전과 동일한 LogOnTable 구조를 Python으로 구현합니다. JSONL 형식으로 일별 파일에 기록합니다.
# src/logger.py
import json
import os
from datetime import datetime
from pathlib import Path
from typing import TypedDict, Literal
LOG_DIR = Path(os.environ.get("LOG_DIR", "./logs"))
LOG_DIR.mkdir(parents=True, exist_ok=True)
class LogEntry(TypedDict):
timestamp: str
user_id: int
chat_id: int
question: str
response_length: int
duration_ms: int
status: Literal["success", "rate_limit", "too_long", "api_error"]
def log_trace(entry: LogEntry) -> None:
date = datetime.now().strftime("%Y-%m-%d")
filepath = LOG_DIR / f"{date}.jsonl"
with filepath.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_error(error: Exception, context: dict) -> None:
date = datetime.now().strftime("%Y-%m-%d")
filepath = LOG_DIR / f"{date}-errors.jsonl"
entry = {
"timestamp": datetime.now().isoformat(),
"error": str(error),
"error_type": type(error).__name__,
"context": context,
}
with filepath.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
Telegram 봇의 핵심 코드입니다. /start, /help, /ask 세 가지 명령을 처리하고, 모든 응답에 ⚖️ Position C footer를 붙입니다.
# src/bot.py
import os
import time
import logging
import sys
from pathlib import Path
from datetime import datetime
# 부모 디렉토리 path 추가
sys.path.insert(0, str(Path(__file__).parent))
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
)
from claude import ask_claude, TooLongError, RateLimitError, ApiError
from logger import log_trace, log_error
from messages import POSITION_C_FOOTER, ERROR_MESSAGES
load_dotenv()
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Welcome 메시지"""
user = update.effective_user
await update.message.reply_text(
f"안녕하세요, {user.first_name}님!\n"
f"질문을 하시려면 /ask 명령을 사용하세요.\n"
f"예: /ask 안녕"
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""도움말"""
await update.message.reply_text(
"[명령어]\n"
"/ask <질문> - Claude에게 질문\n"
"/help - 도움말\n"
"/start - 환영 메시지"
)
async def ask_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/ask 명령 처리"""
question = " ".join(context.args).strip()
if not question:
await update.message.reply_text(ERROR_MESSAGES["no_question"])
return
msg = update.message
chat_id = msg.chat_id
user_id = msg.from_user.id
# 타이핑 표시
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
start = time.time()
status = "success"
response = ""
try:
response = await ask_claude(question)
# Telegram 4096자 한도
if len(response) > 3900:
response = response[:3900] + "...\n(답변 잘림)"
# ⚖️ Position C 본문 추가
full_response = f"{response}\n\n{POSITION_C_FOOTER}"
await msg.reply_text(full_response)
except TooLongError:
status = "too_long"
await msg.reply_text(ERROR_MESSAGES["too_long"])
except RateLimitError:
status = "rate_limit"
await msg.reply_text(ERROR_MESSAGES["rate_limit"])
except ApiError as e:
status = "api_error"
await msg.reply_text(ERROR_MESSAGES["api_error"])
log_error(e, {"user_id": user_id, "question": question})
except Exception as e:
status = "api_error"
await msg.reply_text(ERROR_MESSAGES["api_error"])
log_error(e, {"user_id": user_id, "question": question})
# ★ LogOnTable 로깅
log_trace({
"timestamp": datetime.now().isoformat(),
"user_id": user_id,
"chat_id": chat_id,
"question": question,
"response_length": len(response),
"duration_ms": int((time.time() - start) * 1000),
"status": status,
})
def main():
token = os.environ["TELEGRAM_TOKEN"]
app = Application.builder().token(token).build()
app.add_handler(CommandHandler("start", start_command))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("ask", ask_command))
logger.info("✅ Telegram bot starting...")
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
가상환경을 활성화하고 봇을 실행합니다. Telegram 앱에서 본인 봇을 검색해 테스트하세요.
# 가상환경 활성화 cd ~/tsv-tg-bot source .venv/bin/activate # 실행 python src/bot.py # INFO - ✅ Telegram bot starting... # Telegram 에서 본인 봇 검색 + /start # /ask 안녕 # 응답: # 안녕하세요! 무엇을 도와드릴까요? # # ━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 이 봇은 분석 콘텐츠 보조 도구이며, # ... # ━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 로그 확인 ls logs/ cat logs/$(date +%Y-%m-%d).jsonl | head
Discord 봇과 Telegram 봇을 한 서버에서 동시에 운영할 수 있습니다.
각 봇은 별도 디렉토리·프로세스로 독립적으로 실행됩니다.
단, 지금은 터미널을 닫으면 봇이 종료돼요. 다음 편에서 systemd로 이를 해결합니다.
현재: 두 봇 작동 ✅ — 터미널 유지 필요
다음 편 (6편 systemd + pm2): 서버 재시작 후에도 봇이 자동으로 살아납니다.
- 1️⃣ Python 3.12 + uv — 빠른 패키지 관리, 가상환경 분리
- 2️⃣ Telegram 봇 토큰 — @BotFather에서 발급, 노출 즉시 /revoke
- 3️⃣ 프로젝트 구조 — Discord 봇과 별도 디렉토리, requirements.txt
- 4️⃣ ⚖️ Position C 본문 메시지 — Python에서도 동일 시리즈 메타 원칙
- 5️⃣ Anthropic SDK wrapper — TooLongError·RateLimitError·ApiError 다층 처리
- 6️⃣ LogOnTable 로깅 — TypedDict 활용, JSONL 일별 파일
- 7️⃣ /start · /help · /ask 명령 — 타이핑 표시 + Position C footer 일관
- 8️⃣ 현재 상태: 두 봇 작동 ✅ (종료 시 봇 중단 — 6편 systemd에서 해결)
Python + python-telegram-bot + Anthropic SDK = /ask 명령 봇 첫 작동.
4편 Discord + 5편 Telegram — 동일 메타 원칙으로 두 봇이 한 서버에서 나란히 작동합니다.
다음 편: 6편 — systemd + pm2 + Cloudflare Tunnel (종료해도 봇이 살아있는 구조)