LiteLLM x Parallel Function Calling: GeminiとGPT-4o-miniで複数ファイル一括変更(GoogleColab付)

gemini/gemini-1.5-proで複数の差分ファイルの一覧を作成してそれを元にgpt-4o-miniのParallel Function Callingでgitのパッチを実行することでマルチファイルのファイル変更を実現します。
無料gemini + コスパ最強のgpt-4o-miniのこの構成が最もコスパが高く品質を担保できる構成です。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">LiteLLM x Parallel Function Calling:でGemini 1.5 proとGPT-4o-miniで複数ファイル一括変更してみた！！<br>これならコード作成に優れたGemini 1.5 proを活かしつつParallel Function Callingを使える！！<br>ただ、パッチの+-の精度が少し不安。。。 <a href="https://t.co/PPPREOomaz">https://t.co/PPPREOomaz</a> <a href="https://t.co/1f7bjP63Iq">pic.twitter.com/1f7bjP63Iq</a></p>&mdash; Maki@Sunwood AI Labs. (@hAru_mAki_ch) <a href="https://twitter.com/hAru_mAki_ch/status/1819953163119763729?ref_src=twsrc%5Etfw">August 4, 2024</a></blockquote> 

## はじめに

このGoogle Colabノートでは、LiteLLMライブラリを使って、gemini/gemini-1.5-proとgpt-4o-miniを連携させ、Parallel Function Callingでマルチファイル変更を行う方法をステップバイステップで解説します。初心者の方でも理解しやすいように、丁寧な解説とコード例を豊富に用意しました。

## 目標

* gemini/gemini-1.5-proを使って、コード変更の提案を複数のgit diffパッチとして取得する
* gpt-4o-miniのParallel Function Callingを使って、各パッチを自動的に適用する

## 利点

* **コスト削減:** 無料のgeminiとコスパの良いgpt-4o-miniの組み合わせで費用を抑える
* **効率化:** 自動化により、マルチファイル変更の手間を大幅に削減
* **品質向上:** AIによる提案と自動適用で、コードの品質を向上


## 環境設定

### ライブラリのインストール

必要なライブラリをインストールします。

```python
!pip install -q litellm rich
```

### サンプルリポジトリのクローン

今回はサンプルとして、簡単なリポジトリを使用します。

```python
%cd /content/
!git clone https://github.com/Sunwood-ai-labs/AIRA-Sample00.git
%cd /content/AIRA-Sample00
```

### リポジトリ概要の確認

リポジトリの内容を理解するために、概要を表示します。

```python
!sourcesage
```

```python
!cat /content/AIRA-Sample00/.SourceSageAssets/DOCUMIND/Repository_summary.md
```

### モジュールのインポート

必要なPythonモジュールをインポートします。

```python
import os
from litellm import completion
from rich.console import Console
from rich.tree import Tree
from rich.table import Table
from rich.panel import Panel
from rich.syntax import Syntax
from typing import Dict, Any

console = Console()
```

### APIキーの設定

ご自身のAPIキーを下記に設定してください。

```python
from google.colab import userdata

# APIキーを設定
os.environ["GEMINI_API_KEY"] = userdata.get('GEMINI_API_KEY')
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
```

## 便利関数の定義

レスポンスを見やすく表示するための関数を定義します。

```python
def display_rich_function_call_response(response: Dict[str, Any]) -> None:
    # ... (関数の詳細コードは省略)
```

## gemini/gemini-1.5-proで差分ファイルの一覧を取得

gemini/gemini-1.5-proに、リポジトリの概要と変更要求を伝え、git diffパッチのリストを生成させます。

```python
# リポジトリ概要ファイルの内容を読み込む
repo_summary_path = "/content/AIRA-Sample00/.SourceSageAssets/DOCUMIND/Repository_summary.md"
with open(repo_summary_path, "r") as file:
    repository_summary = file.read()

# プロンプトを作成
prompt = f"""
# 以下のGitHubイシューに対して、リポジトリの概要の情報を踏まえた具体的なコード変更提案のgitのdiffのパッチを生成してください。

# 変更提案のgitのパッチは、ファイルごとに **<response-codeblock file="ファイル名 relative/path/to/file.diff">** を追加してください。
# **</response-codeblock>** タグで囲むのを忘れないでください。
# 既存のファイルが無い場合は新規ファイルの作成を実施してください。

## 例

### 1. 既存ファイルの変更:
<response-codeblock file="example.diff">

```diff
--- a/example/existing_script.py
+++ b/example/existing_script.py
@@ -1,2 +1,2 @@
 def example_function():
-    print("This is an example")
+    print("This is an updated example")
```
</response-codeblock>

### 新規ファイルの作成:
<response-codeblock file="new_file.diff">

```diff
--- /dev/null
+++ b/example/calculation_string_sample.py
@@ -0,0 +1,5 @@
+def calculate_square(number):
+    return number ** 2
+
+def reverse_string(text):
+    return text[::-1]
```
</response-codeblock>

## イシュー
タイトル: exampleフォルダに計算と文字列操作を行う3つのサンプルスクリプトを追加
本文: exampleフォルダ内に、基本的な計算機能と文字列操作機能を持つPythonスクリプトを作成してください。

## 現在のリポジトリの中身:
{repository_summary}

## 変更提案
上記の情報を元に、exampleフォルダに計算と文字列操作を行うサンプルスクリプトを追加するための具体的なコード変更提案を、gitのdiffパッチ形式で生成してください。新しいファイルの作成と、必要に応じて既存ファイルの修正を含めてください。
"""

# ユーザーからの質問を設定
messages = [
    {
        "role": "user",
        "content": prompt,
    }
]

# AIモデルに関数呼び出しを行わせる
response = completion(
    model="gemini/gemini-1.5-pro-latest",
    messages=messages,
)

# レスポンスを表示
display_rich_function_call_response(response)
```


## gpt-4o-miniでパッチを適用

gpt-4o-miniのParallel Function Callingを使って、`apply_git_patch`関数を定義し、生成されたパッチを適用します。

```python
# パッチを適用する関数を定義
def apply_git_patch(patch_content, file_path):
    """
    指定されたgitパッチを適用する関数（実際の実装はここに記述）
    """
    # ここにパッチを適用するロジックを実装
    print(f"パッチを適用しました: {file_path}")
    return f"パッチが正常に適用されました: {file_path}"

# ファンクションコーリング用のツールを定義
tools = [
    {
        "type": "function",
        "function": {
            "name": "apply_git_patch",
            "description": "指定されたgitパッチを適用します",
            "parameters": {
                "type": "object",
                "properties": {
                    "patch_content": {
                        "type": "string",
                        "description": "適用するgitパッチの内容",
                    },
                    "file_path": {
                        "type": "string",
                        "description": "パッチを適用するファイルのパス",
                    },
                },
                "required": ["patch_content", "file_path"],
            },
        },
    }
]


suggest_code = response.choices[0].message.content

# gpt-4o-miniを使用してパッチを適用するためのプロンプトを作成
apply_prompt = f"""
以下のgitパッチ一覧を適用してください。各パッチに対して、apply_git_patch関数を呼び出してパッチを適用してください。

{suggest_code}

各パッチを適用する際は、以下の形式でapply_git_patch関数を呼び出してください：
{{"name": "apply_git_patch", "arguments": {{"patch_content": "パッチの内容", "file_path": "ファイルパス"}}}}
"""

# gpt-4o-miniを使用してパッチを適用するためのファンクションコーリングを実行
patch_response = completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": apply_prompt}],
    tools=tools,
    tool_choice="auto",
)

# レスポンスを表示
display_rich_function_call_response(patch_response)
```

## パッチ適用結果の確認と適用

```python
import json
import subprocess
import tempfile
import re
import os
from datetime import datetime
from loguru import logger
from art import text2art

# ログの設定
logger.add("debug.log", rotation="500 MB")

class HunkHeaderValidator:
    def __init__(self):
        self.hunk_header_pattern = re.compile(r'^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@')

    def validate_and_fix(self, patch_content):
        logger.info("パッチ内容の検証と修正を開始します")
        lines = patch_content.split('\n')
        if len(lines) >= 3:
            header_match = self.hunk_header_pattern.match(lines[2])
            if header_match:
                start_original, start_new = map(int, header_match.groups())
                new_line_count = sum(1 for line in lines[3:] if line.startswith('+'))
                old_header = lines[2]
                lines[2] = f"@@ -{start_original},0 +{start_new},{new_line_count} @@"
                logger.info(f"ハンクヘッダーを修正しました: {old_header} -> {lines[2]}")
            else:
                logger.warning("ハンクヘッダーが見つかりませんでした")
        else:
            logger.warning("パッチ内容が短すぎます")
        logger.info("パッチ内容の検証と修正を完了しました")
        return '\n'.join(lines)

def save_patch(patch_content, file_path):
    """
    パッチをファイルに保存する関数
    """
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    patch_dir = "saved_patches"
    os.makedirs(patch_dir, exist_ok=True)

    file_name = os.path.basename(file_path)
    save_path = os.path.join(patch_dir, f"{timestamp}_{file_name}.patch")

    with open(save_path, 'w') as f:
        f.write(patch_content)

    logger.info(f"パッチを保存しました: {save_path}")
    return save_path

def apply_git_patch(patch_content, file_path):
    """
    指定されたgitパッチをsubprocessを使用して適用する関数
    """
    logger.info(f"パッチの適用を開始: {file_path}")

    # パッチ内容の検証と修正
    validator = HunkHeaderValidator()
    fixed_patch_content = validator.validate_and_fix(patch_content)

    # パッチの保存
    saved_patch_path = save_patch(fixed_patch_content, file_path)
    logger.debug(f"修正済みの一時パッチファイルを作成: {saved_patch_path}")

    # パッチの適用
    cmd = ['git', 'apply', saved_patch_path, '--ignore-whitespace', '--whitespace=fix']
    cmd_debug = " ".join(cmd)
    logger.debug(f"パッチの適用コマンド: {cmd_debug}")
    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode == 0:
        logger.success(f"パッチが正常に適用されました: {file_path}")
        return f"パッチが正常に適用されました: {file_path} (保存されたパッチ: {saved_patch_path})"
    else:
        logger.error(f"パッチの適用に失敗: {result.stderr}")
        return f"パッチの適用に失敗しました: {file_path} (保存されたパッチ: {saved_patch_path})\nエラー: {result.stderr}"

def process_patch_response(response):
    logger.info("パッチレスポンスの処理を開始")

    results = []
    for tool_call in response.choices[0].message.tool_calls:
        if tool_call.function.name == 'apply_git_patch':
            args = json.loads(tool_call.function.arguments)
            patch_content = args['patch_content']
            file_path = args['file_path']
            logger.debug(f"パッチ適用: {file_path}")
            result = apply_git_patch(patch_content, file_path)
            results.append(result)

    return results


# パッチを適用
logger.info("パッチ適用プロセスを開始")
patch_results = process_patch_response(patch_response)
logger.info("パッチ適用プロセスが完了しました")
```

上記コードでは、`apply_git_patch`関数を定義し、gpt-4o-miniが生成したパッチを実際に適用する処理を行っています。適用結果は`patch_results`変数に格納されます。

最後に、適用されたパッチを確認し、問題なければ`git add`と`git commit`で変更をコミットしてください。

## まとめ

このノートでは、LiteLLMを使ってgemini/gemini-1.5-proとgpt-4o-miniを連携させ、Parallel Function Callingでマルチファイル変更を行う方法を紹介しました。この方法を活用することで、効率的にコード変更を行い、開発プロセスを加速させることができます。

## 📒ノートブック

https://colab.research.google.com/drive/1_9L3uGJYpAXDZSE6M4RPMuNrVBivsoys?usp=sharing

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>