Claude 3.5/3.7 Sonnetに追加されたText editor toolを使えばClineが自作できる（かもしれない）

Claudeに新しいツール「テキストエディター」ツールが追加されました。

https://docs.anthropic.com/en/docs/build-with-claude/tool-use/text-editor-tool

ユースケースとして以下のものが挙げられています。

- コードのデバッグ
- コードのリファクタリング
- ドキュメント生成
- テスト生成

ん？これは、今流行りの**Cline**ですね！

ということで、この新しいツールを使ってみて、Clineっぽいものができるのか、検証しました。

## 環境

ドキュメント記載内容に沿って進めることにしましたので、Anthropic SDKを使いつつ、呼び出すのはAmazon BedrockのClaude 3.7 Sonnetとします。

## 実装手順

まず、ライブラリーをインストールします

```shell
pip install anthropic boto3
```

まず、ドキュメントに従い、APIを呼び出す部分を作成します。

clientを生成する部分のみ、Bedrockを使用する形式に変更しました。

:::note
私のAWSアカウントはClaude 3.7 Sonnetのクオーターが低く設定されてしまっているため、リトライ回数を多めにしています
:::

```python
import anthropic

client = anthropic.AnthropicBedrock(aws_region="us-east-1", max_retries=10) # ※１

response = client.messages.create(
    model="claude-3-7-sonnet-20250219",
    max_tokens=1024,
    tools=[
        {
            "type": "text_editor_20250124",
            "name": "str_replace_editor"
        }
    ],
    messages=[
        {
            "role": "user", 
            "content": "There's a syntax error in my primes.py file. Can you help me fix it?"
        }
    ]
)
```

toolsの指定が見慣れない形をしていますね。

通常、ツールを指定する場合は以下のように定義情報を指定しますが、テキストエディターツールの場合はツール定義の代わりに`type`と`name`を指定します。

```python:通常のツール定義
tools=[
    {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA",
                }
            },
            "required": ["location"],
        },
    }
],
```

テキストエディターツールは、以下の5つの機能（コマンド）で構成されています。

- **view**コマンド
    ファイルの内容を要求するツール。オプションで取得する行数範囲が指定される場合がある
- **str_replace**コマンド
    ファイルの文字列置換を要求する機能
- **create**コマンド
    ファイルの新規作成を要求する機能
- **insert**コマンド
    ファイルの特定の位置への文字列追加を要求する機能
- **undo_edit**コマンド
    最後の修正をもとに戻すことを要求する機能

:::note
テキストエディターツールが正確に動作するように、Claudeのモデルのファインチューニングをしているのではないかな？と、推測しています。
:::

Claudeがテキストエディターツールを使用すると判断した際には、以下のような`tool_use`ブロックがレスポンスに含まれます。

```python
{
  "type": "tool_use",
  "id": "toolu_01AbCdEfGhIjKlMnOpQrStU",
  "name": "str_replace_editor",
  "input": {
    "command": "view",
    "path": "primes.py"
  }
}
```

ツール呼び出しを要求された後のツール処理は自前で実装する必要があります。
ドキュメントでは以下のような`handle_editor_tool`関数の記述があります。
が、

```python
def handle_editor_tool(tool_call):
    input_params = tool_call.input
    command = input_params.get('command', '')
    file_path = input_params.get('path', '')
    
    if command == 'view':
        # Read and return file contents
        pass
    elif command == 'str_replace':
        # Replace text in file
        pass
    elif command == 'create':
        # Create new file
        pass
    elif command == 'insert':
        # Insert text at location
        pass
    elif command == 'undo_edit':
        # Restore from backup
        pass
```

。。。

なんと、肝心な部分の実装がありません😅

実装できなくはなさそうですが、なんやかんやめんどくさそう（？）なので、参考になるものを探しました。

で、**見つけました！**

AnthropicがGitHubに公開している[Quickstarts](https://github.com/anthropics/anthropic-quickstarts)に、[Computer use](https://docs.anthropic.com/en/docs/agents-and-tools/computer-use)のデモが含まれています。

https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo

実はComputer useの機能の一部としてテキストエディターツールが含まれているため、その実装を拝借することにしました。

computer_use_demo/tools以下を丸ごとコピーしてきます。

```console:tree
.
├── app.py
└── tools
    ├── base.py
    ├── bash.py
    ├── collection.py
    ├── computer.py
    ├── edit.py
    ├── groups.py
    ├── __init__.py
    └── run.py
```

`handle_editor_tool`関数を作成します。

```python
from tools.edit import CLIResult, EditTool20250124, ToolResult

async def handle_editor_tool(tool_call) -> CLIResult | ToolResult:
    """
    テキストエディタツールを実行し、その結果を返します。
    """
    input_params = tool_call.input
    command = input_params.get("command", "")
    file_path = input_params.get("path", "")
    file_text = input_params.get("file_text")
    old_str = input_params.get("old_str")
    new_str = input_params.get("new_str")
    insert_line = input_params.get("insert_line")
    view_range = input_params.get("view_range")

    editor = EditTool20250124()

    try:
        result = await editor(
            command=command,
            path=file_path,
            file_text=file_text,
            old_str=old_str,
            new_str=new_str,
            insert_line=insert_line,
            view_range=view_range,
        )

        return result
    except Exception as e:
        return ToolResult(output=str(e))
```

あとは、ツール呼び出しが繰り返し要求されることを想定し、ループ処理を記述します。
最終、完成したのがこのようになります。

```python
import asyncio

import anthropic

from tools.edit import CLIResult, EditTool20250124, ToolResult

client = anthropic.AnthropicBedrock(aws_region="us-east-1", max_retries=10)


async def handle_editor_tool(tool_call) -> CLIResult | ToolResult:
    """
    テキストエディタツールを実行し、その結果を返します。
    """
    input_params = tool_call.input
    command = input_params.get("command", "")
    file_path = input_params.get("path", "")
    file_text = input_params.get("file_text")
    old_str = input_params.get("old_str")
    new_str = input_params.get("new_str")
    insert_line = input_params.get("insert_line")
    view_range = input_params.get("view_range")

    editor = EditTool20250124()

    try:
        result = await editor(
            command=command,
            path=file_path,
            file_text=file_text,
            old_str=old_str,
            new_str=new_str,
            insert_line=insert_line,
            view_range=view_range,
        )
    return result
    except Exception as e:
        return ToolResult(output=str(e))


async def main(content):
    messages = []
    messages.append(
        {
            "role": "user"
            "content": content,
        }
    )

    while True:
        # Claude 3.7 Sonnetに呼び出し
        response = client.messages.create(
            model="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
            max_tokens=10240,
            tools=[
                {"type": "text_editor_20250124", "name": "str_replace_editor"},
            ],
            messages=messages,
        )

        # 応答がtool_useでない場合、出力を表示してループを終了します。
        if not response.stop_reason == "tool_use":

            # 出力してBreak
            for content in response.content:
                if content.type == "text":
                    print(content.text)
                if content.type == "thinking":
                    print(content.thinking)
                if content.type == "redacted_thinking":
                    print(content.data)

            break

        # tool_useの場合、ツールの結果を処理します。
        tool_result_message = {"role": "user", "content": []}

        for content in response.content:
            if content.type == "text":
                print(content.text)
            if content.type == "thinking":
                print(content.thinking)
            if content.type == "redacted_thinking":
                print(content.data)
            if content.type == "tool_use":
                # Execute the tool based on command
                result = await handle_editor_tool(content)

                print(result.output)

                # Return result to Claude
                tool_result = {
                    "type": "tool_result",
                    "tool_use_id": content.id,
                    "content": result.output,
                }}
                tool_result_message["content"].append(tool_result)

        messages.append({"role": "assistant", "content": response.content})
        messages.append(tool_result_message)


if __name__ == "__main__":

    prompt = "There's a syntax error in my primes.py file. Can you help me fix it?"

    print(f"入力： {prompt}")

    asyncio.run(main(prompt))
```

## Clineっぽいことができるかな？

早速やってみましょう。

Promptを以下のように指定しました。

```primes.pyにシンタックスエラーがあります。修正してください。現在のディレクトリは`{os.getcwd()}`です。```

:::note
何度か試したところ、パスの情報を渡さないと期待動作にならないことがわかったので、カレントディレクトリを渡すようにしました
:::

ちなみに仕込んだシンタックスエラーは以下の3つです。

- Returnのインデントがおかしい
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/c0434952-957a-49f8-94e5-367329b0d1b0.png)
- JSONのカンマが不足している
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/32659998-4309-4e00-95aa-1a9d9674a99d.png)
- JSONの閉じ括弧が多い
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/b1e09d77-bebb-43ee-9f5d-59ef47c757c9.png)

:::note
IDEのエラー表示がなかったら、`目Grep`でも結構しんどやつです。
:::

---

実行してみましょう。

> 😃ユーザー入力：
> primes.pyにシンタックスエラーがあります。修正してください。現在のディレクトリは`/workspaces/2503_cline/claude-new-tool`です。

> 🤖Claude:
> シンタックスエラーがあるという`primes.py`ファイルを確認し、修正するために、まずファイルの内容を確認しましょう。

<details><summary>Tool実行結果</summary>

```
Here's the result of running `cat -n` on /workspaces/2503_cline/claude-new-tool/primes.py:
     1  import asyncio
     2
     3  import anthropic
     4
     5  from tools.edit import CLIResult, EditTool20250124, ToolResult
     6
     7  client = anthropic.AnthropicBedrock(aws_region="us-east-1", max_retries=10)
     8
     9
    10  async def handle_editor_tool(tool_call) -> CLIResult | ToolResult:
    11      """
    12      テキストエディタツールを実行し、その結果を返します。
    13      """
    14      input_params = tool_call.input
    15      command = input_params.get("command", "")
    16      file_path = input_params.get("path", "")
    17      file_text = input_params.get("file_text")
    18      old_str = input_params.get("old_str")
    19      new_str = input_params.get("new_str")
    20      insert_line = input_params.get("insert_line")
    21      view_range = input_params.get("view_range")
    22
    23      editor = EditTool20250124()
    24
    25      try:
    26          result = await editor(
    27              command=command,
    28              path=file_path,
    29              file_text=file_text,
    30              old_str=old_str,
    31              new_str=new_str,
    32              insert_line=insert_line,
    33              view_range=view_range,
    34          )
    35      return result
    36      except Exception as e:
    37          return ToolResult(output=str(e))
    38
    39
    40  async def main(content):
    41      messages = []
    42      messages.append(
    43          {
    44              "role": "user"
    45              "content": content,
    46          }
    47      )
    48
    49      while True:
    50          # Claude 3.7 Sonnetに呼び出し
    51          response = client.messages.create(
    52              model="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
    53              max_tokens=10240,
    54              tools=[
    55                  {"type": "text_editor_20250124", "name": "str_replace_editor"},
    56              ],
    57              messages=messages,
    58          )
    59
    60          # 応答がtool_useでない場合、出力を表示してループを終了します。
    61          if not response.stop_reason == "tool_use":
    62
    63              # 出力してBreak
    64              for content in response.content:
    65                  if content.type == "text":
    66                      print(content.text)
    67                  if content.type == "thinking":
    68                      print(content.thinking)
    69                  if content.type == "redacted_thinking":
    70                      print(content.data)
    71
    72              break
    73
    74          # tool_useの場合、ツールの結果を処理します。
    75          tool_result_message = {"role": "user", "content": []}
    76
    77          for content in response.content:
    78              if content.type == "text":
    79                  print(content.text)
    80              if content.type == "thinking":
    81                  print(content.thinking)
    82              if content.type == "redacted_thinking":
    83                  print(content.data)
    84              if content.type == "tool_use":
    85                  # Execute the tool based on command
    86                  result = asyncio.run(handle_editor_tool(content))
    87
    88                  print(result.output)
    89
    90                  # Return result to Claude
    91                  tool_result = {
    92                      "type": "tool_result",
    93                      "tool_use_id": content.id,
    94                      "content": result.output,
    95                  }}
    96                  tool_result_message["content"].append(tool_result)
    97
    98          messages.append({"role": "assistant", "content": response.content})
    99          messages.append(tool_result_message)
   100
   101
   102  if __name__ == "__main__":
   103
   104      prompt = "There's a syntax error in my primes.py file. Can you help me fix it?"
   105
   106      print(f"入力： {prompt}")
   107
   108      asyncio.run(main(prompt))
   109
```
</details>

> 🤖Claude:
> コードを確認したところ、いくつかのシンタックスエラーがあります。修正が必要な箇所は以下の通りです：
> 
> 1. 35行目と36行目の間でインデントエラー - `try`ブロックの後に`return`がきてから`except`が来ています。
> 2. 44行目の`"role": "user"`の後にカンマが抜けています。
> 3. 95行目の閉じ括弧が余分になっています。
> 
> これらのエラーを修正します：

<details><summary>Tool実行結果</summary>

```
The file /workspaces/2503_cline/claude-new-tool/primes.py has been edited. Here's the result of running `cat -n` on a snippet of /workspaces/2503_cline/claude-new-tool/primes.py:
    21      view_range = input_params.get("view_range")
    22
    23      editor = EditTool20250124()
    24
    25      try:
    26          result = await editor(
    27              command=command,
    28              path=file_path,
    29              file_text=file_text,
    30              old_str=old_str,
    31              new_str=new_str,
    32              insert_line=insert_line,
    33              view_range=view_range,
    34          )
    35          return result
    36      except Exception as e:
    37          return ToolResult(output=str(e))
    38
    39
    40  async def main(content):
    41      messages = []
Review the changes and make sure they are as expected. Edit the file again if necessary.
```

</details>

<details><summary>Tool実行結果</summary>

```
The file /workspaces/2503_cline/claude-new-tool/primes.py has been edited. Here's the result of running `cat -n` on a snippet of /workspaces/2503_cline/claude-new-tool/primes.py:
    38
    39
    40  async def main(content):
    41      messages = []
    42      messages.append(
    43          {
    44              "role": "user",
    45              "content": content,
    46          }
    47      )
    48
    49      while True:
    50          # Claude 3.7 Sonnetに呼び出し
    51          response = client.messages.create(
Review the changes and make sure they are as expected. Edit the file again if necessary.
```

</details>

<details><summary>Tool実行結果</summary>

```
The file /workspaces/2503_cline/claude-new-tool/primes.py has been edited. Here's the result of running `cat -n` on a snippet of /workspaces/2503_cline/claude-new-tool/primes.py:
    86                  result = asyncio.run(handle_editor_tool(content))
    87
    88                  print(result.output)
    89
    90                  # Return result to Claude
    91                  tool_result = {
    92                      "type": "tool_result",
    93                      "tool_use_id": content.id,
    94                      "content": result.output,
    95                  }
    96                  tool_result_message["content"].append(tool_result)
    97
    98          messages.append({"role": "assistant", "content": response.content})
    99          messages.append(tool_result_message)
   100
Review the changes and make sure they are as expected. Edit the file again if necessary.
```

</details>

> 🤖Claude:
> `primes.py`のシンタックスエラーを修正しました。以下の3つの問題を解決しました：
> 
> 1. **インデントエラー（35-36行目）**: 
>    - `try`ブロック内で`return result`が正しくインデントされていなかったため、`try`と`except`の間に来ていました
>    - `return result`を`try`ブロック内に適切に配置しました
> 
> 2. **カンマの欠落（44行目）**:
>    - 辞書内の`"role": "user"`の後にカンマが欠けていました
>    - カンマを追加して`"role": "user",`に修正しました
> 
> 3. **余分な閉じ括弧（95行目）**:
>    - `tool_result`辞書定義の後に余分な閉じ括弧`}`がありました
>    - 余分な閉じ括弧を削除しました
> 
> これらの修正により、コードのシンタックスエラーは解決されました。プログラムが正常に実行できるようになりました。

すごい！！！
ですが、Claude 3.7 Sonnetにとってはお茶の子さいさいなのかもしれませんね。

:::note
念の為お伝えしておくと、変なところを見つけただけじゃなくて、修正まで完璧です
:::


## ちなみに

Anthropic SDKを使わずConverse APIで利用する場合は以下のようになります。

`toolConfig`に`tools`を指定するとバリデーションエラーになるので、`additionalModelRequestFields`で指定する必要があります。

```python
import boto3

client = boto3.client("bedrock-runtime")

response = client.converse(
    modelId="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
    inferenceConfig={
        "maxTokens": 10240,
    },
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "text": "There's a syntax error in my primes.py file. Can you help me fix it?",
                }
            ],
        }
    ],
    additionalModelRequestFields={
        "tools": [
            {"type": "text_editor_20250124", "name": "str_replace_editor"},
        ],
    },
)
```