Langfuse 프롬프트 GitHub 통합

Langfuse 프롬프트 GitHub 통합 (GitHub Integration for Langfuse Prompts)

Langfuse 프롬프트를 GitHub와 통합하는 방법은 두 가지가 있어요.

출처: 문서

본문

  • GitHub Repository Dispatch — 프롬프트가 변경될 때 CI/CD 워크플로우를 트리거해요. 추가 인프라가 필요 없어요.
  • GitHub로 프롬프트 동기화(Sync) — 프롬프트를 저장소의 특정 파일에 저장해요. 프롬프트 버전 변경을 듣고 저장소에 커밋하는 웹훅 서버가 필요해요.

GitHub Actions 트리거하기

repository_dispatch 이벤트를 사용해 Langfuse 프롬프트가 변경될 때 GitHub Actions 워크플로우를 트리거할 수 있어요.

1. GitHub 워크플로우 만들기

.github/workflows/langfuse-ci.yml:

name: Langfuse Prompt CI
on:
  repository_dispatch:
    types: [langfuse-prompt-update]
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          echo "Testing prompt: ${{ github.event.client_payload.prompt.name }} v${{ github.event.client_payload.prompt.version }}"
          # Add your test commands
          # npm test
          # python -m pytest

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: contains(github.event.client_payload.prompt.labels, 'production')
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          echo "Deploying ${{ github.event.client_payload.prompt.name }} v${{ github.event.client_payload.prompt.version }}"
          # Your deployment commands

웹훅 데이터 접근: github.event.client_payload.*를 사용해 프롬프트 데이터에 접근할 수 있어요:

# Example: Access webhook data in your workflow
- name: Process prompt data
  run: |
    echo "Action: ${{ github.event.client_payload.action }}"
    echo "Prompt: ${{ github.event.client_payload.prompt.name }}"
    echo "Version: ${{ github.event.client_payload.prompt.version }}"
    echo "Labels: ${{ github.event.client_payload.prompt.labels }}"

- name: Deploy only production prompts
  if: contains(github.event.client_payload.prompt.labels, 'production')
  run: echo "Deploying production prompt"

2. Actions용 GitHub 토큰 만들기

단계:

  • GitHub Settings > Developer settings > Personal access tokens
  • 새 토큰 생성(classic 또는 fine-grained)
  • 범위 선택(아래 표 참고)

| 토큰 유형 | 필요한 권한 | | Personal Access Token (classic) | repo 범위(공개 저장소) 또는 repo 범위(비공개 저장소) | | Fine-grained PAT 또는 GitHub App | actions에 대한 읽기·쓰기 |

3. Langfuse에서 GitHub Action 구성하기

  • Langfuse 프로젝트에서 Prompts > Automations로 이동.
  • Create Automation 클릭.
  • GitHub Repository Dispatch 선택.
  • 자동화 구성:
    • Dispatch URL: https://api.github.com/repos/{owner}/{repo}/dispatches ({owner}{repo}를 당신의 값으로 교체)
    • Event Type: langfuse-prompt-update (GitHub 워크플로우의 타입과 일치해야 함)
    • GitHub Token: GitHub Personal Access Token 입력. 안전하게 저장됨.

4. GitHub Actions 통합 테스트

  • Langfuse에서 production 라벨로 프롬프트 업데이트
  • GitHub Actions 탭에서 트리거된 워크플로우 확인
  • testdeploy 작업이 모두 성공적으로 실행되는지 확인

Langfuse 프롬프트를 저장소로 동기화하기

프롬프트 버전 웹훅을 사용해 Langfuse에서 GitHub로 프롬프트 변경을 자동으로 동기화해요. 이렇게 하면 프롬프트의 버전 관리를 활성화하고 CI/CD 워크플로우를 트리거할 수 있어요.

동기화 워크플로우 개요

Langfuse에서 새 프롬프트 버전을 저장할 때마다 GitHub 저장소에 자동으로 커밋돼요. 이 설정으로 프롬프트가 변경될 때 CI/CD 워크플로우도 트리거할 수 있어요.

동기화 사전 요구사항

  • Langfuse 프로젝트: 프로젝트 소유자(Project Owner) 접근 권한이 있는 프롬프트 설정
  • GitHub 저장소: 프롬프트를 저장할 공개 또는 비공개 저장소
  • GitHub PAT: 최소 요구 권한을 가진 Personal Access Token(자세한 내용은 2단계 참고)
  • Python 3.9+ (아래 예시 기준, 다른 언어도 가능) 및 FastAPI, Uvicorn, httpx, Pydantic
  • 웹훅 서버용 공개 HTTPS 엔드포인트 (Render, Fly.io, Heroku 등)

1단계: Langfuse에서 프롬프트 웹훅 구성

  • Langfuse 프로젝트에서 Prompts > Webhooks로 이동
  • Create Webhook 클릭
  • (선택) 이벤트 필터: 어떤 프롬프트 버전 이벤트에 웹훅을 받을지 필터링(기본: created, updated, deleted)
  • 엔드포인트 URL 설정: https:///webhook/prompt
  • 저장 후 Signing Secret 복사

참고: 엔드포인트는 2xx 상태 코드를 반환해야 해요. Langfuse는 실패한 웹훅을 지수 백오프로 재시도해요.

샘플 웹훅 페이로드

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-07-10T10:30:00Z",
  "type": "prompt-version",
  "action": "created",
  "prompt": {
    "id": "prompt_abc123",
    "name": "movie-critic",
    "version": 3,
    "projectId": "xyz789",
    "labels": ["production", "latest"],
    "prompt": "As a {{criticLevel}} movie critic, rate {{movie}} out of 10.",
    "type": "text",
    "config": { "...": "..." },
    "commitMessage": "Improved critic persona",
    "tags": ["entertainment"],
    "createdAt": "2024-07-10T10:30:00Z",
    "updatedAt": "2024-07-10T10:30:00Z"
  }
}

2단계: 동기화용 GitHub 저장소 및 토큰 준비

GitHub 자격 증명으로 .env 파일을 만드세요:

GITHUB_TOKEN=<your_github_pat_here>
GITHUB_REPO_OWNER=<github_username_or_org>
GITHUB_REPO_NAME=<repo_name>
# (Optional) GITHUB_FILE_PATH=langfuse_prompt.json
# (Optional) GITHUB_BRANCH=main
# (Optional) REQUIRED_LABEL=production

플레이스홀더를 실제 값으로 교체하세요. 서버는 기본적으로 main 브랜치의 langfuse_prompt.json에 프롬프트를 커밋해요. REQUIRED_LABEL이 설정되면 해당 라벨이 있는 프롬프트만 GitHub에 동기화돼요.

동기화용 GitHub PAT 권한

웹훅이 작동하려면 GitHub Personal Access Token에 최소 권한이 필요해요:

| 권한 유형 | 필요한 권한 | | Required Permissions | Contents: Read and write, Metadata: Read-only | | 레거시 토큰 범위 | 공개 저장소: public_repo 범위, 비공개 저장소: repo 범위 |

3단계: FastAPI 웹훅 서버 구현

이 FastAPI 서버로 main.py를 만드세요:

from typing import Any, Dict
from uuid import UUID
import json
import base64

import httpx
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from fastapi import FastAPI, HTTPException, Body

class GitHubSettings(BaseSettings):
    """GitHub repository configuration."""
    GITHUB_TOKEN: str
    GITHUB_REPO_OWNER: str
    GITHUB_REPO_NAME: str
    GITHUB_FILE_PATH: str = "langfuse_prompt.json"
    GITHUB_BRANCH: str = "main"
    REQUIRED_LABEL: str = ""  # Optional: only sync prompts with this label

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=True
    )

config = GitHubSettings()

class LangfuseEvent(BaseModel):
    """Langfuse webhook event structure."""
    id: UUID = Field(description="Event identifier")
    timestamp: str = Field(description="Event timestamp")
    type: str = Field(description="Event type")
    action: str = Field(description="Performed action")
    prompt: Dict[str, Any] = Field(description="Prompt content")

async def sync(event: LangfuseEvent) -> Dict[str, Any]:
    """Synchronize prompt data to GitHub repository."""
    # Check if prompt has required label (if specified)
    if config.REQUIRED_LABEL:
        prompt_labels = event.prompt.get("labels", [])
        if config.REQUIRED_LABEL not in prompt_labels:
            return {"skipped": f"Prompt does not have required label '{config.REQUIRED_LABEL}'"}

    api_endpoint = f"https://api.github.com/repos/{config.GITHUB_REPO_OWNER}/{config.GITHUB_REPO_NAME}/contents/{config.GITHUB_FILE_PATH}"

    request_headers = {
        "Authorization": f"Bearer {config.GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json"
    }

    content_json = json.dumps(event.prompt, indent=2)
    encoded_content = base64.b64encode(content_json.encode("utf-8")).decode("utf-8")

    name = event.prompt.get("name", "unnamed")
    version = event.prompt.get("version", "unknown")
    message = f"{event.action}: {name} v{version}"

    payload = {
        "message": message,
        "content": encoded_content,
        "branch": config.GITHUB_BRANCH
    }

    async with httpx.AsyncClient() as http_client:
        try:
            existing = await http_client.get(api_endpoint, headers=request_headers, params={"ref": config.GITHUB_BRANCH})
            if existing.status_code == 200:
                payload["sha"] = existing.json().get("sha")
        except Exception:
            pass

        try:
            response = await http_client.put(api_endpoint, headers=request_headers, json=payload)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Repository sync failed: {str(e)}")

app = FastAPI(title="Langfuse GitHub Sync", version="1.0")

@app.post("/webhook/prompt", status_code=201)
async def receive_webhook(event: LangfuseEvent = Body(...)):
    """Process Langfuse webhook and sync to GitHub."""
    result = await sync(event)
    return {
        "status": "synced",
        "commit_info": result.get("commit", {}),
        "file_info": result.get("content", {})
    }

@app.get("/status")
async def health_status():
    """Service health check."""
    return {"healthy": True}

서버는 웹훅 페이로드를 검증하고, 필요하면 기존 파일 SHA를 조회하며, 설명적인 커밋 메시지로 프롬프트 변경을 GitHub에 커밋해요.

의존성

의존성을 설치하세요:

pip install fastapi uvicorn pydantic-settings httpx

로컬에서 실행

로컬에서 실행:

uvicorn main:app --reload --port 8000

http://localhost:8000/health에서 헬스 엔드포인트를 테스트하세요. 웹훅 테스트를 위해 ngrok 등을 사용해 localhost를 노출하세요.

4단계: 서버 배포 및 연결

  • 배포: Render, Fly.io, Heroku 등을 사용하세요. 환경 변수를 설정하고 HTTPS를 활성화하세요.
  • 웹훅 업데이트: Langfuse에서 웹훅을 편집하고 URL을 https://your-domain.com/webhook/prompt로 설정하세요.
  • 테스트: Langfuse에서 프롬프트를 업데이트하고 GitHub 저장소에 새 커밋이 나타나는지 확인하세요.

보안 고려사항

  • 서명 검증: 서명 비밀과 x-langfuse-signature 헤더를 사용해 요청을 검증하세요
  • PAT 범위 제한: 특정 저장소로 제한된 fine-grained 토큰을 사용하세요
  • 재시도 처리: 구현은 멱등(idempotent)이라 중복 이벤트가 충돌하는 커밋을 만들지 않아요

더 알아보기 (Learn more)