Skip to main content
LangSmith는 프롬프트를 생성하고, 테스트하며, 반복 작업을 수행할 수 있는 협업 인터페이스를 제공합니다. 런타임에 애플리케이션으로 프롬프트를 동적으로 가져올 수 있지만, 프롬프트를 자체 데이터베이스나 버전 관리 시스템과 동기화하는 것을 선호할 수도 있습니다. 이러한 워크플로우를 지원하기 위해 LangSmith는 웹훅을 통해 프롬프트 업데이트 알림을 받을 수 있습니다. GitHub와 프롬프트를 동기화하는 이유는 무엇인가요?
  • 버전 관리: 익숙한 시스템에서 애플리케이션 코드와 함께 프롬프트의 버전을 관리할 수 있습니다.
  • CI/CD 통합: 중요한 프롬프트가 변경될 때 자동화된 스테이징 또는 프로덕션 배포를 트리거할 수 있습니다.
Prompt Webhook Diagram

사전 준비사항

시작하기 전에 다음 사항이 설정되어 있는지 확인하세요:
  1. GitHub 계정: 표준 GitHub 계정.
  2. GitHub 리포지토리: LangSmith 프롬프트 매니페스트가 저장될 새 리포지토리를 생성하거나 기존 리포지토리를 선택합니다. 애플리케이션 코드와 동일한 리포지토리이거나 프롬프트 전용 리포지토리일 수 있습니다.
  3. GitHub 개인 액세스 토큰(PAT):
    • LangSmith 웹훅은 GitHub와 직접 상호작용하지 않으며, 사용자가 생성한 중개 서버를 호출합니다.
    • 이 서버는 리포지토리에 인증하고 커밋을 수행하기 위해 GitHub PAT가 필요합니다.
    • repo 범위를 포함해야 합니다(공개 리포지토리의 경우 public_repo로 충분).
    • **GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic)**로 이동합니다.
    • **Generate new token (classic)**을 클릭합니다.
    • 이름을 지정하고(예: “LangSmith Prompt Sync”), 만료일을 설정하며, 필요한 범위를 선택합니다.
    • Generate token을 클릭하고 즉시 복사하세요 — 다시 표시되지 않습니다.
    • 토큰을 안전하게 저장하고 서버에 환경 변수로 제공합니다.

LangSmith “프롬프트 커밋”과 웹훅 이해하기

LangSmith에서 프롬프트에 변경 사항을 저장하면 본질적으로 새로운 버전이나 “프롬프트 커밋”을 생성하는 것입니다. 이러한 커밋이 웹훅을 트리거할 수 있습니다. 웹훅은 새로운 프롬프트 매니페스트를 포함하는 JSON 페이로드를 전송합니다.
{
  "prompt_id": "f33dcb51-eb17-47a5-83ca-64ac8a027a29",
  "prompt_name": "My Prompt",
  "commit_hash": "commit_hash_1234567890",
  "created_at": "2021-01-01T00:00:00Z",
  "created_by": "Jane Doe",
  "manifest": {
    "lc": 1,
    "type": "constructor",
    "id": ["langchain", "schema", "runnable", "RunnableSequence"],
    "kwargs": {
      "first": {
        "lc": 1,
        "type": "constructor",
        "id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
        "kwargs": {
          "messages": [
            {
              "lc": 1,
              "type": "constructor",
              "id": [
                "langchain_core",
                "prompts",
                "chat",
                "SystemMessagePromptTemplate"
              ],
              "kwargs": {
                "prompt": {
                  "lc": 1,
                  "type": "constructor",
                  "id": [
                    "langchain_core",
                    "prompts",
                    "prompt",
                    "PromptTemplate"
                  ],
                  "kwargs": {
                    "input_variables": [],
                    "template_format": "mustache",
                    "template": "You are a chatbot."
                  }
                }
              }
            },
            {
              "lc": 1,
              "type": "constructor",
              "id": [
                "langchain_core",
                "prompts",
                "chat",
                "HumanMessagePromptTemplate"
              ],
              "kwargs": {
                "prompt": {
                  "lc": 1,
                  "type": "constructor",
                  "id": [
                    "langchain_core",
                    "prompts",
                    "prompt",
                    "PromptTemplate"
                  ],
                  "kwargs": {
                    "input_variables": ["question"],
                    "template_format": "mustache",
                    "template": "{{question}}"
                  }
                }
              }
            }
          ],
          "input_variables": ["question"]
        }
      },
      "last": {
        "lc": 1,
        "type": "constructor",
        "id": ["langchain", "schema", "runnable", "RunnableBinding"],
        "kwargs": {
          "bound": {
            "lc": 1,
            "type": "constructor",
            "id": ["langchain", "chat_models", "openai", "ChatOpenAI"],
            "kwargs": {
              "temperature": 1,
              "top_p": 1,
              "presence_penalty": 0,
              "frequency_penalty": 0,
              "model": "gpt-4.1-mini",
              "extra_headers": {},
              "openai_api_key": {
                "id": ["OPENAI_API_KEY"],
                "lc": 1,
                "type": "secret"
              }
            }
          },
          "kwargs": {}
        }
      }
    }
  }
}
프롬프트 커밋에 대한 LangSmith 웹훅은 일반적으로 워크스페이스 수준에서 트리거된다는 점을 이해하는 것이 중요합니다. 즉, LangSmith 워크스페이스 내의 모든 프롬프트가 수정되고 “프롬프트 커밋”이 저장되면 웹훅이 실행되고 프롬프트의 업데이트된 매니페스트를 전송합니다. 페이로드는 프롬프트 ID로 식별할 수 있습니다. 수신 서버는 이를 염두에 두고 설계되어야 합니다.

웹훅 수신을 위한 FastAPI 서버 구현

프롬프트가 업데이트될 때 LangSmith로부터 웹훅 알림을 효과적으로 처리하려면 중개 서버 애플리케이션이 필요합니다. 이 서버는 LangSmith가 보내는 HTTP POST 요청의 수신자 역할을 합니다. 이 가이드에서는 데모 목적으로 이 역할을 수행하기 위한 간단한 FastAPI 애플리케이션 생성을 개요로 설명합니다. 이 공개 액세스 가능한 서버는 다음을 담당합니다:
  1. 웹훅 요청 수신: 들어오는 HTTP POST 요청을 수신합니다.
  2. 페이로드 파싱: 요청 본문에서 JSON 형식의 프롬프트 매니페스트를 추출하고 해석합니다.
  3. GitHub에 커밋: 업데이트된 프롬프트 매니페스트를 포함하여 지정된 GitHub 리포지토리에 프로그래밍 방식으로 새 커밋을 생성합니다. 이를 통해 프롬프트가 버전 관리되고 LangSmith에서 변경된 내용과 동기화됩니다.
배포를 위해 Render.com(적합한 무료 티어 제공), Vercel, Fly.io 또는 기타 클라우드 제공업체(AWS, GCP, Azure)와 같은 플랫폼을 사용하여 FastAPI 애플리케이션을 호스팅하고 공개 URL을 얻을 수 있습니다. 서버의 핵심 기능에는 웹훅 수신을 위한 엔드포인트, 매니페스트 파싱을 위한 로직, 커밋을 관리하기 위한 GitHub API와의 통합(인증을 위해 개인 액세스 토큰 사용)이 포함됩니다.
main.py이 서버는 LangSmith로부터 들어오는 웹훅을 수신하고 수신된 프롬프트 매니페스트를 GitHub 리포지토리에 커밋합니다.
import base64
import json
import uuid
from typing import Any, Dict
import httpx
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

# --- Configuration ---
class AppConfig(BaseSettings):
    """
    Application configuration model.
    Loads settings from environment variables.
    """
    GITHUB_TOKEN: str
    GITHUB_REPO_OWNER: str
    GITHUB_REPO_NAME: str
    GITHUB_FILE_PATH: str = "prompt_manifest.json"
    GITHUB_BRANCH: str = "main"
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding='utf-8',
        extra='ignore'
    )

settings = AppConfig()

# --- Pydantic Models ---
class WebhookPayload(BaseModel):
    """
    Defines the expected structure of the incoming webhook payload.
    """
    prompt_id: UUID = Field(
        ...,
        description="The unique identifier for the prompt."
    )
    prompt_name: str = Field(
        ...,
        description="The name/title of the prompt."
    )
    commit_hash: str = Field(
        ...,
        description="An identifier for the commit event that triggered the webhook."
    )
    created_at: str = Field(
        ...,
        description="Timestamp indicating when the event was created (ISO format preferred)."
    )
    created_by: str = Field(
        ...,
        description="The name of the user who created the event."
    )
    manifest: Dict[str, Any] = Field(
        ...,
        description="The main content or configuration data to be committed to GitHub."
    )

# --- GitHub Helper Function ---
async def commit_manifest_to_github(payload: WebhookPayload) -> Dict[str, Any]:
    """
    Helper function to commit the manifest directly to the configured branch.
    """
    github_api_base_url = "https://api.github.com"
    repo_file_url = (
        f"{github_api_base_url}/repos/{settings.GITHUB_REPO_OWNER}/"
        f"{settings.GITHUB_REPO_NAME}/contents/{settings.GITHUB_FILE_PATH}"
    )
    headers = {
        "Authorization": f"Bearer {settings.GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    manifest_json_string = json.dumps(payload.manifest, indent=2)
    content_base64 = base64.b64encode(manifest_json_string.encode('utf-8')).decode('utf-8')
    commit_message = f"feat: Update {settings.GITHUB_FILE_PATH} via webhook - commit {payload.commit_hash}"
    data_to_commit = {
        "message": commit_message,
        "content": content_base64,
        "branch": settings.GITHUB_BRANCH,
    }
    async with httpx.AsyncClient() as client:
        current_file_sha = None
        try:
            params_get = {"ref": settings.GITHUB_BRANCH}
            response_get = await client.get(repo_file_url, headers=headers, params=params_get)
            if response_get.status_code == 200:
                current_file_sha = response_get.json().get("sha")
            elif response_get.status_code != 404: # If not 404 (not found), it's an unexpected error
                response_get.raise_for_status()
        except httpx.HTTPStatusError as e:
            error_detail = f"GitHub API error (GET file SHA): {e.response.status_code} - {e.response.text}"
            print(f"[ERROR] {error_detail}")
            raise HTTPException(status_code=e.response.status_code, detail=error_detail)
        except httpx.RequestError as e:
            error_detail = f"Network error connecting to GitHub (GET file SHA): {str(e)}"
            print(f"[ERROR] {error_detail}")
            raise HTTPException(status_code=503, detail=error_detail)
        if current_file_sha:
            data_to_commit["sha"] = current_file_sha
        try:
            response_put = await client.put(repo_file_url, headers=headers, json=data_to_commit)
            response_put.raise_for_status()
            return response_put.json()
        except httpx.HTTPStatusError as e:
            error_detail = f"GitHub API error (PUT content): {e.response.status_code} - {e.response.text}"
            if e.response.status_code == 409: # Conflict
                error_detail = (
                    f"GitHub API conflict (PUT content): {e.response.text}. "
                    "This might be due to an outdated SHA or branch protection rules."
                )
            elif e.response.status_code == 422: # Unprocessable Entity
                error_detail = (
                    f"GitHub API Unprocessable Entity (PUT content): {e.response.text}. "
                    f"Ensure the branch '{settings.GITHUB_BRANCH}' exists and the payload is correctly formatted."
                )
            print(f"[ERROR] {error_detail}")
            raise HTTPException(status_code=e.response.status_code, detail=error_detail)
        except httpx.RequestError as e:
            error_detail = f"Network error connecting to GitHub (PUT content): {str(e)}"
            print(f"[ERROR] {error_detail}")
            raise HTTPException(status_code=503, detail=error_detail)

# --- FastAPI Application ---
app = FastAPI(
    title="Minimal Webhook to GitHub Commit Service",
    description="Receives a webhook and commits its 'manifest' part directly to a GitHub repository.",
    version="0.1.0",
)

@app.post("/webhook/commit", status_code=201, tags=["GitHub Webhooks"])
async def handle_webhook_direct_commit(payload: WebhookPayload = Body(...)):
    """
    Webhook endpoint to receive events and commit DIRECTLY to the configured branch.
    """
    try:
        github_response = await commit_manifest_to_github(payload)
        return {
            "message": "Webhook received and manifest committed directly to GitHub successfully.",
            "github_commit_details": github_response.get("commit", {}),
            "github_content_details": github_response.get("content", {})
        }
    except HTTPException:
        raise # Re-raise if it's an HTTPException from the helper
    except Exception as e:
        error_message = f"An unexpected error occurred: {str(e)}"
        print(f"[ERROR] {error_message}")
        raise HTTPException(status_code=500, detail="An internal server error occurred.")

@app.get("/health", status_code=200, tags=["Health"])
async def health_check():
    """
    A simple health check endpoint.
    """
    return {"status": "ok", "message": "Service is running."}

# To run this server (save as main.py):
# 1. Install dependencies: pip install fastapi uvicorn pydantic pydantic-settings httpx python-dotenv
# 2. Create a .env file with your GitHub token and repo details.
# 3. Run with Uvicorn: uvicorn main:app --reload
# 4. Deploy to a public platform like Render.com.
이 서버의 주요 측면:
  • 구성(.env): GITHUB_TOKEN, GITHUB_REPO_OWNER, GITHUB_REPO_NAME이 포함된 .env 파일이 필요합니다. GITHUB_FILE_PATH(기본값: LangSmith_prompt_manifest.json) 및 GITHUB_BRANCH(기본값: main)를 사용자 정의할 수도 있습니다.
  • GitHub 상호작용: commit_manifest_to_github 함수는 현재 파일의 SHA를 가져오고(업데이트하기 위해) 새 매니페스트 콘텐츠를 커밋하는 로직을 처리합니다.
  • 웹훅 엔드포인트(/webhook/commit): LangSmith 웹훅이 대상으로 하는 URL 경로입니다.
  • 오류 처리: GitHub API 상호작용을 위한 기본 오류 처리가 포함되어 있습니다.
이 서버를 선택한 플랫폼(예: Render)에 배포하고 공개 URL(예: https://prompt-commit-webhook.onrender.com)을 기록해두세요.

LangSmith에서 웹훅 구성하기

FastAPI 서버가 배포되고 공개 URL이 있으면 LangSmith에서 웹훅을 구성할 수 있습니다:
  1. LangSmith 워크스페이스로 이동합니다.
  2. Prompts 섹션으로 이동합니다. 여기에 프롬프트 목록이 표시됩니다. LangSmith Prompts section
  3. Prompts 페이지의 오른쪽 상단에서 + Webhook 버튼을 클릭합니다.
  4. 웹훅을 구성하는 양식이 표시됩니다: LangSmith Webhook configuration modal
    • Webhook URL: 배포된 FastAPI 서버 엔드포인트의 전체 공개 URL을 입력합니다. 예제 서버의 경우 https://prompt-commit-webhook.onrender.com/webhook/commit이 됩니다.
    • Headers(선택 사항):
      • LangSmith가 각 웹훅 요청과 함께 전송할 사용자 정의 헤더를 추가할 수 있습니다.
  5. 웹훅 테스트: LangSmith는 “Send Test Notification” 버튼을 제공합니다. 이를 사용하여 서버로 샘플 페이로드를 전송하세요. 서버 로그(예: Render에서)를 확인하여 요청을 수신하고 성공적으로 처리하는지(또는 문제를 디버깅하기 위해) 확인하세요.
  6. 웹훅 구성을 저장합니다.

실제 워크플로우

Workflow Diagram showing: User saves prompt in LangSmith, LangSmith sends webhook to FastAPI Server, which interacts with GitHub to update files 이제 모든 것이 설정되었으므로 다음과 같이 진행됩니다:
  1. 프롬프트 수정: 사용자(개발자 또는 비기술 팀원)가 LangSmith UI에서 프롬프트를 수정하고 저장하여 새로운 “프롬프트 커밋”을 생성합니다.
  2. 웹훅 트리거: LangSmith가 이 새로운 프롬프트 커밋을 감지하고 구성된 웹훅을 트리거합니다.
  3. HTTP 요청: LangSmith가 FastAPI 서버의 공개 URL(예: https://prompt-commit-webhook.onrender.com/webhook/commit)로 HTTP POST 요청을 전송합니다. 이 요청의 본문에는 전체 워크스페이스에 대한 JSON 프롬프트 매니페스트가 포함됩니다.
  4. 서버가 페이로드 수신: FastAPI 서버의 엔드포인트가 요청을 수신합니다.
  5. GitHub 커밋: 서버가 요청 본문에서 JSON 매니페스트를 파싱합니다. 그런 다음 구성된 GitHub 개인 액세스 토큰, 리포지토리 소유자, 리포지토리 이름, 파일 경로 및 브랜치를 사용하여:
    • 지정된 브랜치의 리포지토리에 매니페스트 파일이 이미 존재하는지 확인하여 SHA를 가져옵니다(기존 파일을 업데이트하는 데 필요).
    • 최신 프롬프트 매니페스트로 새 커밋을 생성하여 파일을 생성하거나 이미 존재하는 경우 업데이트합니다. 커밋 메시지는 LangSmith로부터의 업데이트임을 나타냅니다.
  6. 확인: GitHub 리포지토리에 새 커밋이 나타나는 것을 확인할 수 있습니다. Manifest commited to Github
이제 LangSmith 프롬프트를 GitHub와 성공적으로 동기화했습니다!

단순 커밋을 넘어서

예제 FastAPI 서버는 전체 프롬프트 매니페스트의 직접 커밋을 수행합니다. 그러나 이것은 시작점일 뿐입니다. 서버의 기능을 확장하여 더욱 정교한 작업을 수행할 수 있습니다:
  • 세분화된 커밋: 매니페스트를 파싱하고 리포지토리에서 더 세분화된 구조를 선호하는 경우 개별 프롬프트 파일에 변경 사항을 커밋합니다.
  • CI/CD 트리거: 커밋 대신(또는 커밋에 추가로) 서버가 스테이징 환경을 배포하거나 테스트를 실행하거나 새 애플리케이션 버전을 빌드하기 위해 CI/CD 파이프라인(예: Jenkins, GitHub Actions, GitLab CI)을 트리거하도록 합니다.
  • 데이터베이스/캐시 업데이트: 애플리케이션이 데이터베이스나 캐시에서 프롬프트를 로드하는 경우 이러한 저장소를 직접 업데이트합니다.
  • 알림: 프롬프트 변경 사항에 대해 Slack, 이메일 또는 기타 커뮤니케이션 채널로 알림을 전송합니다.
  • 선택적 처리: LangSmith 페이로드 내의 메타데이터(사용 가능한 경우, 예: 어떤 특정 프롬프트가 변경되었는지 또는 누구에 의해 변경되었는지)를 기반으로 다른 로직을 적용할 수 있습니다.

Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I