별책부록 5편
별책부록 5편
Python + python-telegram-bot
uv + Anthropic SDK + Position C 완전 boilerplate
📑 이 챕터에서 다룰 내용

4편 (Node.js + Discord)과 동일한 메타 원칙에 Python + Telegram을 결합합니다. 한 서버에서 두 봇을 동시에 운영할 수 있어요.

항목내용
예상 시간1~2시간
완성 결과Telegram 채팅에서 /ask → Claude API → 답변
💡 4편 (Discord) vs 5편 (Telegram) 일관성
  • 동일 시리즈 메타 원칙 (⚖️ Position C, LogOnTable, 다층 보호)
  • 동일 흐름 (질문 → API → 응답 + footer)
  • 다른 SDK (discord.js vs python-telegram-bot)
  • 한 서버에 두 봇 동시 운영 가능
4-1 Python 3.12 설치 (uv 권장) 🔗

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
4-2 Telegram 봇 토큰 발급 🔗

Telegram 봇은 @BotFather를 통해 만듭니다. Telegram 앱에서 바로 진행할 수 있어요.

📘 BotFather 단계별 안내
[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 - 도움말
⚠️ 토큰은 1Password·Bitwarden에 보관하세요

토큰이 노출되면 즉시 BotFather에서 /revoke로 무효화하고 재발급받으세요.

절대 코드에 직접 쓰거나 Git에 push하지 마세요.

4-3 프로젝트 구조 (Python) 🔗

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/
4-4 .env + .gitignore 🔗

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-5 ⚖️ Position C 메시지 (Python) 🔗

4편 (TypeScript)과 동일한 내용을 Python으로 구현합니다. 시리즈 메타 원칙을 플랫폼과 언어에 관계없이 일관되게 적용합니다.

💻 src/messages.py
# 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 안녕",
}
4-6 Anthropic SDK Wrapper (Python) 🔗

Python용 Anthropic SDK wrapper입니다. TypeScript 버전과 동일한 에러 처리 구조를 Python 커스텀 예외로 구현합니다.

💻 src/claude.py
# 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
4-7 LogOnTable 로깅 (Python) 🔗

TypeScript 버전과 동일한 LogOnTable 구조를 Python으로 구현합니다. JSONL 형식으로 일별 파일에 기록합니다.

💻 src/logger.py
# 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")
4-8 ★ 메인 봇 코드 (Python + Telegram) 🔗

Telegram 봇의 핵심 코드입니다. /start, /help, /ask 세 가지 명령을 처리하고, 모든 응답에 ⚖️ Position C footer를 붙입니다.

💻 src/bot.py
# 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()
4-9 첫 실행 + 검증 🔗

가상환경을 활성화하고 봇을 실행합니다. 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로 이를 해결합니다.

📘 현재 상태 vs 다음 편

현재: 두 봇 작동 ✅ — 터미널 유지 필요

다음 편 (6편 systemd + pm2): 서버 재시작 후에도 봇이 자동으로 살아납니다.

📌 5편 정리
  • 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 (종료해도 봇이 살아있는 구조)

📘
별책부록 도우미
질문하기 OK
안녕하세요! Telegram 봇 설정에 대해 무엇이든 물어보세요. 본문에서 찾아 답변해드릴게요. 👇