GLM과 GitHub 커넥터로 레거시 Python 코드 현대화하기

GLM과 GitHub 커넥터로 레거시 Python 코드 현대화하기 (Modernize legacy Python code using GLM and the GitHub connector)

zai-glm-5-2(GLM) 모델과 GitHub 커넥터를 사용해서 리포지토리에서 오래된 Python 코드를 읽고 현대화된 버전을 만드는 쿡북이에요.

출처: 문서

본문

API status: 에이전트는 client.beta.agents를 사용해요. 대화는 client.beta.conversations를 사용해요. 이들은 beta 엔드포인트라 변경될 수 있어요.

사전 요구 사항 (Prerequisites)

이 쿡북을 완료하려면 다음이 필요해요:

  • Python 3.10+
  • Mistral 계정과 API 키
  • GitHub 계정 (스크립트가 OAuth 인증을 처리해요)

환경 설정 (Environment setup)

설치 (Install)

Mistral Python SDK와 .env 파일에서 API 키를 로드하기 위한 python-dotenv를 설치하세요:

pip install mistralai python-dotenv

필요한 환경 변수 (Required environment variables)

이 쿡북을 완료하려면 Mistral API 키가 필요해요. Studio에서 API 키 섹션으로 이동해서 새 API 키를 만드세요.

예시 .env 파일을 복사하고 Mistral API 키를 추가하세요:

cp .env.example .env
MISTRAL_API_KEY=your-mistral-api-key
GITHUB_REPO=mistralai/cookbook
GITHUB_BRANCH=main

GITHUB_REPO와 GITHUB_BRANCH 변수는 기본적으로 main 브랜치의 mistralai/cookbook 리포지토리를 사용해요. 다른 리포지토리의 파일을 현대화하려면 이 값을 변경하세요.

Step 1 — 클라이언트 초기화 (Initialize the client)

프로젝트 디렉터리에 modernize_code.py를 만드세요:

touch modernize_code.py

파일을 열고 import와 클라이언트 초기화를 추가하세요. 나머지 단계들은 에이전트, 대화, 출력 로직을 구성해요.

"""Modernize legacy Python code using GLM and the GitHub connector."""

import asyncio
import os
import re
from pathlib import Path

from dotenv import load_dotenv

from mistralai.client import Mistral

load_dotenv()

# Step 1 — Initialize the client
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

async def main() -> None:
    # Step 2 — Define the modernization target
    # Step 3 — Authenticate the GitHub connector
    # Step 4 — Create the agent with the GitHub connector
    # Step 5 — Start the conversation and extract results
    # Step 6 — Save the modernized code
    pass

if __name__ == "__main__":
    asyncio.run(main())

Step 2 — 현대화 대상 정의 (Define the modernization target)

현대화할 리포지토리, 브랜치, 파일 경로를 지정해요. 리포지토리와 브랜치는 환경 변수에서 읽으며, 기본적으로 main 브랜치의 mistralai/cookbook 리포지토리를 사용해요. 이 쿡북은 이 리포지토리의 legacy_app/ 디렉터리에 있는, 의도적으로 오래된 Python 파일 두 개를 대상으로 해요.

클라이언트 초기화 아래에 다음 상수를 추가하세요:

# Step 2 — Define the modernization target
REPO = os.environ.get("GITHUB_REPO", "mistralai/cookbook")
BRANCH = os.environ.get("GITHUB_BRANCH", "main")
FILE_PATHS = [
    "mistral/agents/glm_code_modernizer/legacy_app/app.py",
    "mistral/agents/glm_code_modernizer/legacy_app/utils.py",
]

이 파일들은 Python 2/3.5 시대 패턴(%-포매팅, os.path, 수동 open()/close(), 베어 except:, type() 체크, sys.argv 파싱, 타입 힌트 없음)으로 작성된 CLI 할 일 목록 관리자를 포함해요.

Step 3 — GitHub 커넥터 인증 (Authenticate the GitHub connector)

github_app 커넥터는 스크립트를 실행할 때마다 OAuth 인증을 요구해요. 스크립트는 get_auth_url_async를 호출해서 인증 URL을 얻고, 이 URL을 브라우저에서 열어 접근을 허용합니다.

main의 맨 위에 다음을 추가하세요:

    # Step 3 — Authenticate the GitHub connector
    auth_result = await client.beta.connectors.get_auth_url_async(
        connector_id_or_name="github_app",
    )
    print(f"Authenticate the GitHub connector:\n{auth_result.auth_url}")
    input("Press Enter once you've completed the OAuth flow in your browser...")

스크립트를 실행하면 GitHub OAuth URL이 출력돼요. 브라우저에서 열고 접근을 승인한 뒤 터미널로 돌아와 Enter를 눌러 계속하세요.

Step 4 — GitHub 커넥터로 에이전트 생성 (Create the agent with the GitHub connector)

GLM과 GitHub 커넥터를 짝지은 에이전트를 만드세요. 에이전트의 지시(instructions)는 어떤 패턴을 현대화하고 출력을 어떻게 포맷할지 정확히 알려줘요.

인증 단계 뒤 main 안에, 정리를 보장하기 위해 try/finally 블록으로 감싸서 다음을 추가하세요:

    agent_id: str | None = None
    try:
        # Step 4 — Create the agent with the GitHub connector
        file_list = "\n".join(f"- `{path}`" for path in FILE_PATHS)
        agent = await client.beta.agents.create_async(
            name="code_modernizer",
            description="Reads legacy Python files from GitHub and produces modernized versions",
            model="zai-glm-5-2",
            instructions=(
                "You are an expert Python developer who modernizes legacy code.\n\n"
                "IMPORTANT: You MUST use the GitHub connector to read each file's "
                "actual contents from the repository. Do NOT guess or invent code. "
                "Read the real source code first, then modernize it.\n\n"
                "Your workflow:\n"
                "1. Use the GitHub connector to read each file from the repository\n"
                "2. Analyze the actual code you retrieved for legacy patterns\n"
                "3. Produce a modernized version of THAT SAME CODE — same logic, "
                "same structure, same functionality, but with modern Python idioms\n\n"
                "Apply these modernizations to the code you read:\n"
                "- Replace %-formatting with f-strings\n"
                "- Replace os.path with pathlib.Path\n"
                "- Replace manual open()/close() with 'with' statements\n"
                "- Replace json.loads(f.read()) with json.load(f)\n"
                "- Replace bare except: with specific exceptions\n"
                "- Replace type() checks with isinstance()\n"
                "- Replace mutable default arguments with None sentinels\n"
                "- Replace sys.argv parsing with argparse\n"
                "- Add type hints to all functions\n"
                "- Replace == True/False comparisons with truthiness checks\n"
                "- Replace range(len(...)) with enumerate or direct iteration\n\n"
                "Return each modernized file in a separate ```python code fence. "
                "Include a comment with the original filename at the top of each block. "
                "The modernized code must preserve the original functionality exactly."
            ),
            tools=[
                {
                    "type": "connector",
                    "connector_id": "github_app",
                },
            ],
        )
        agent_id = agent.id
        print(f"Created agent: {agent.name} ({agent.id})")

두 가지를 기억하세요:

  • 모델 (Model): zai-glm-5-2는 코드를 생성하고 변환하도록 설계된 코드 생성 모델이에요.
  • 커넥터 (Connector): "github_app"은 이름으로 내장 GitHub 커넥터를 참조해요. 에이전트는 이 커넥터를 사용해서 리포지토리 파일을 서버 측에서 읽어요.

Step 5 — 대화 시작과 결과 추출 (Start the conversation and extract results)

에이전트와 대화를 시작하세요. 프롬프트는 어떤 리포지토리와 파일을 읽을지 알려줘요. GitHub 커넥터가 파일 접근을 처리해요 — 모델이 파일을 읽고, 패턴을 분석하고, 단일 응답으로 현대화된 코드를 반환해요.

main 위에 다음 헬퍼 함수들을 추가하세요:

def extract_python_blocks(text: str) -> list[str]:
    """Extract Python code blocks from the model response."""
    return re.findall(r"```python\s*\n(.*?)```", text, re.DOTALL)

def get_response_text(response) -> str:
    """Extract text content from a conversation response."""
    parts = []
    for output in response.outputs:
        if output.type == "message.output":
            content = output.content
            if isinstance(content, str):
                parts.append(content)
            else:
                parts.append(
                    "".join(
                        chunk.text if hasattr(chunk, "text") else str(chunk)
                        for chunk in content
                    )
                )
    return "\n".join(parts)

그다음 에이전트 생성 뒤 try 블록 안에서 계속하세요:

        # Step 5 — Start the conversation
        print(f"Reading files from {REPO} and modernizing...")
        print("This may take a few minutes.\n")

        response = await client.beta.conversations.start_async(
            agent_id=agent.id,
            inputs=[
                {
                    "role": "user",
                    "content": (
                        f"Read these files from the `{REPO}` repository "
                        f"(branch: `{BRANCH}`) using the GitHub connector, "
                        f"then produce modernized versions of each:\n\n{file_list}\n\n"
                        "Return each modernized file in a ```python code fence "
                        "with a comment at the top indicating the original filename."
                    ),
                }
            ],
            timeout_ms=600_000,
        )

        text = get_response_text(response)

timeout_ms=600_000 파라미터는 10분 타임아웃을 설정해요. GLM은 큰 코드 출력을 생성하므로, 이 여유가 요청이 타임아웃되는 걸 방지해요.

model 대신 agent_id를 전달하면, 대화가 에이전트의 모델, 지시, 도구를 자동으로 사용해요. Conversations API는 모든 도구 호출을 서버 측에서 처리해요 — 에이전트가 GitHub 커넥터를 호출해서 파일을 읽기로 결정하고, 내용을 처리하고, 현대화된 코드를 한 번의 응답 주기로 반환해요.

Step 6 — 현대화된 코드 저장 (Save the modernized code)

응답에서 Python 코드 블록을 추출하고 각각을 modernized/ 디렉터리에 저장하세요:

        # Step 6 — Save the modernized code
        code_blocks = extract_python_blocks(text)

        if not code_blocks:
            print("No Python code blocks found in the response.")
            print("\nRaw response:\n")
            print(text)
            return

        output_dir = Path("modernized")
        output_dir.mkdir(exist_ok=True)

        filenames = [Path(p).name for p in FILE_PATHS]
        for i, block in enumerate(code_blocks):
            name = filenames[i] if i < len(filenames) else f"file_{i}.py"
            output_path = output_dir / name
            output_path.write_text(block.strip() + "\n", encoding="utf-8")
            print(f"Saved: {output_path}")

        print(f"\nModernized {len(code_blocks)} file(s) in {output_dir}/")

정리 (Cleanup)

작업이 끝나면 에이전트를 삭제하세요. finally 블록은 오류가 발생해도 정리가 보장되게 해줘요:

    finally:
        # Cleanup — Delete the agent
        if agent_id:
            await client.beta.agents.delete_async(agent_id=agent_id)
            print(f"Deleted agent: {agent_id}")

실행 (Run)

모든 단계가 준비되면 스크립트를 실행하세요:

python modernize_code.py

스크립트가 GitHub 커넥터를 OAuth로 인증하라고 안내한 뒤, GLM 에이전트를 만들고, 리포지토리에서 레거시 파일을 읽고, 현대화된 버전을 만들어 modernized/ 디렉터리에 저장해요.

예시 출력:

Authenticate the GitHub connector:
https://github.com/login/oauth/authorize?client_id=...&scope=repo+...
Press Enter once you've completed the OAuth flow in your browser...

Created agent: code_modernizer (a1b2c3d4-5678-90ab-cdef-1234567890ab)
Reading files from mistralai/cookbook and modernizing...
This may take a few minutes.

Saved: modernized/app.py
Saved: modernized/utils.py

Modernized 2 file(s) in modernized/
Deleted agent: a1b2c3d4-5678-90ab-cdef-1234567890ab

다른 대상 시도 (Try different targets)

GITHUB_REPO와 GITHUB_BRANCH를 .env에서 업데이트해서 다른 리포지토리의 파일을 현대화할 수 있어요. 스크립트에서 FILE_PATHS를 바꾸거나, 에이전트의 지시를 조정해서 다른 현대화 패턴에 집중할 수도 있어요.

Django views 파일을 현대화하려면:

.env를 업데이트하세요:

GITHUB_REPO=your-org/your-django-app

그다음 스크립트에서 FILE_PATHS를 바꾸세요:

FILE_PATHS = ["myapp/views.py"]

특정 브랜치를 대상으로 하려면:

GITHUB_BRANCH=feature/legacy-cleanup

Python 대신 JavaScript를 현대화하려면:

에이전트 지시를 JavaScript 패턴(예: var를 const/let으로, 콜백을 async/await로, CommonJS를 ES 모듈로)을 대상으로 업데이트하고, 코드 펜스 추출이 ```javascript 블록을 찾도록 바꾸세요.

전체 스크립트 (Complete script)

참고용으로, 모든 단계를 결합한 전체 스크립트입니다:

"""Modernize legacy Python code using GLM and the GitHub connector."""

import asyncio
import os
import re
from pathlib import Path

from dotenv import load_dotenv

from mistralai.client import Mistral

load_dotenv()

# Step 1 — Initialize the client
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

# Step 2 — Define the modernization target
REPO = os.environ.get("GITHUB_REPO", "mistralai/cookbook")
BRANCH = os.environ.get("GITHUB_BRANCH", "main")
FILE_PATHS = [
    "mistral/agents/glm_code_modernizer/legacy_app/app.py",
    "mistral/agents/glm_code_modernizer/legacy_app/utils.py",
]

def extract_python_blocks(text: str) -> list[str]:
    """Extract Python code blocks from the model response."""
    return re.findall(r"```python\s*\n(.*?)```", text, re.DOTALL)

def get_response_text(response) -> str:
    """Extract text content from a conversation response."""
    parts = []
    for output in response.outputs:
        if output.type == "message.output":
            content = output.content
            if isinstance(content, str):
                parts.append(content)
            else:
                parts.append(
                    "".join(
                        chunk.text if hasattr(chunk, "text") else str(chunk)
                        for chunk in content
                    )
                )
    return "\n".join(parts)

async def main() -> None:
    # Step 3 — Authenticate the GitHub connector
    auth_result = await client.beta.connectors.get_auth_url_async(
        connector_id_or_name="github_app",
    )
    print(f"Authenticate the GitHub connector:\n{auth_result.auth_url}")
    input("Press Enter once you've completed the OAuth flow in your browser...")

    agent_id: str | None = None
    try:
        # Step 4 — Create the agent with the GitHub connector
        file_list = "\n".join(f"- `{path}`" for path in FILE_PATHS)
        agent = await client.beta.agents.create_async(
            name="code_modernizer",
            description="Reads legacy Python files from GitHub and produces modernized versions",
            model="zai-glm-5-2",
            instructions=(
                "You are an expert Python developer who modernizes legacy code.\n\n"
                "IMPORTANT: You MUST use the GitHub connector to read each file's "
                "actual contents from the repository. Do NOT guess or invent code. "
                "Read the real source code first, then modernize it.\n\n"
                "Your workflow:\n"
                "1. Use the GitHub connector to read each file from the repository\n"
                "2. Analyze the actual code you retrieved for legacy patterns\n"
                "3. Produce a modernized version of THAT SAME CODE — same logic, "
                "same structure, same functionality, but with modern Python idioms\n\n"
                "Apply these modernizations to the code you read:\n"
                "- Replace %-formatting with f-strings\n"
                "- Replace os.path with pathlib.Path\n"
                "- Replace manual open()/close() with 'with' statements\n"
                "- Replace json.loads(f.read()) with json.load(f)\n"
                "- Replace bare except: with specific exceptions\n"
                "- Replace type() checks with isinstance()\n"
                "- Replace mutable default arguments with None sentinels\n"
                "- Replace sys.argv parsing with argparse\n"
                "- Add type hints to all functions\n"
                "- Replace == True/False comparisons with truthiness checks\n"
                "- Replace range(len(...)) with enumerate or direct iteration\n\n"
                "Return each modernized file in a separate ```python code fence. "
                "Include a comment with the original filename at the top of each block. "
                "The modernized code must preserve the original functionality exactly."
            ),
            tools=[
                {
                    "type": "connector",
                    "connector_id": "github_app",
                },
            ],
        )
        agent_id = agent.id
        print(f"Created agent: {agent.name} ({agent.id})")

        # Step 5 — Start the conversation
        print(f"Reading files from {REPO} and modernizing...")
        print("This may take a few minutes.\n")

        response = await client.beta.conversations.start_async(
            agent_id=agent.id,
            inputs=[
                {
                    "role": "user",
                    "content": (
                        f"Read these files from the `{REPO}` repository "
                        f"(branch: `{BRANCH}`) using the GitHub connector, "
                        f"then produce modernized versions of each:\n\n{file_list}\n\n"
                        "Return each modernized file in a ```python code fence "
                        "with a comment at the top indicating the original filename."
                    ),
                }
            ],
            timeout_ms=600_000,
        )

        text = get_response_text(response)

        # Step 6 — Save the modernized code
        code_blocks = extract_python_blocks(text)

        if not code_blocks:
            print("No Python code blocks found in the response.")
            print("\nRaw response:\n")
            print(text)
            return

        output_dir = Path("modernized")
        output_dir.mkdir(exist_ok=True)

        filenames = [Path(p).name for p in FILE_PATHS]
        for i, block in enumerate(code_blocks):
            name = filenames[i] if i < len(filenames) else f"file_{i}.py"
            output_path = output_dir / name
            output_path.write_text(block.strip() + "\n", encoding="utf-8")
            print(f"Saved: {output_path}")

        print(f"\nModernized {len(code_blocks)} file(s) in {output_dir}/")

    finally:
        # Cleanup — Delete the agent
        if agent_id:
            await client.beta.agents.delete_async(agent_id=agent_id)
            print(f"Deleted agent: {agent_id}")

if __name__ == "__main__":
    asyncio.run(main())

요약 (Summary)

이 쿡북은 GLM의 코드 생성과 GitHub 커넥터를 결합해서 코드 현대화 파이프라인을 만드는 방법을 보여줬어요 — 에이전트가 리포지토리에서 직접 레거시 파일을 읽고, 관용적이고 현대적인 Python을 반환해요.

만든 것 (What you built):

  • GitHub에서 오래된 Python 파일을 읽고 현대화된 버전을 만드는 코드 현대화 도구
  • 서버 측 파일 접근을 위해 GLM(zai-glm-5-2)과 GitHub 커넥터를 짝지은 에이전트
  • 모델 응답에서 코드 블록을 추출해서 로컬에 저장하는 파이프라인

사용한 Mistral 기능 (Mistral features used):

  • zai-glm-5-2 모델을 사용한 Agents API (beta)
  • 서버 측 도구 실행을 위한 Conversations API (beta)
  • OAuth 인증을 사용한 GitHub 커넥터 (github_app)
  • 긴 코드 생성을 위한 확장 타임아웃 (timeout_ms)

스크립트를 여러분 자신의 리포지토리를 가리키게 해서 실제 레거시 코드를 현대화해 보세요. 커넥터에 대한 자세한 내용은 커넥터 문서를 참고하세요.

더 알아보기 (Learn more)