以下の内容はhttps://touch-sp.hatenablog.com/entry/2025/04/03/221940より取得しました。


【SmolAgents】Playwright MCP を使ってみる。

はじめに

Microsoftが開発しているPlaywrightはブラウザ自動化機能を提供するライブラリです。Playwrihgt MCPはそのModel Context Protocol (MCP)サーバになります。

これを使うことによって言語モデルとPlaywrightを簡単に結びつけることができるようになるので、自然言語で命令を出すことができるようになります。

今回やったこと

https://github.com/huggingface/smolagents にアクセスしてスクリーンショットを撮って保存して下さい。

GoogleのGemma-3-12b(4bit量子化)を使って完全にローカルな環境で実行しています。

ハマった点

なぜか必ず「/tmp」フォルダに保存します。
そして必ず保存されるファイルのフォーマットはPDFになりました。

「〇〇に保存して下さい」と保存先を指定しても効果ありませんでした。

保存した後に「保存したファイルを〇〇にコピーして下さい」と言ってもしてくれませんでした。

対策

Dockerコンテナをサンドボックスとして実行しています。コンテナ作成時に「/tmp」フォルダをコンテナ外の任意のフォルダに関連付けることで対策しました。

self.container = self.client.containers.run(
    self.image_name,
    command="tail -f /dev/null",
    detach=True,
    tty=True,
    extra_hosts={"host.docker.internal": "host-gateway"},
    network_mode="bridge",
    ports={'7860/tcp': 7860},
    volumes={
        "/home/hoge/data": {"bind": "/tmp", "mode": "rw"} # <=ここ
    }
)

結果


SmolAgentsを実行して終了するまでの過程を日本語に訳して残しておきます。

SmolAgentsが苦労した末にゴールにたどり着く様子をぜひ見て下さい。

1回のエラーであきらめないところに感心します。

Step1

指定されたURLに移動してスクリーンショットを撮り、PDFとして保存する必要があります。まず、browser_navigateを使ってURLに移動します。次に、browser_screenshotを使ってスクリーンショットを撮ります。最後に、browser_save_as_pdfを使ってページをPDFとして保存します。
url = "https://github.com/huggingface/smolagents"
result = browser_navigate(url={"url": url})
print(result)

このコードを実行してエラーになりました。

Step2

エラーは、browser_navigateのurlパラメータは文字列でなければならないことを示していますが、私はそれを辞書として渡しました。URLを文字列として直接渡す必要があります。
url = "https://github.com/huggingface/smolagents"
result = browser_navigate(url=url)
print(result)

このコードを実行して成功しました。

Step3

URLへの移動に成功しました。では、現在のページのスクリーンショットを撮ります。
screenshot = browser_screenshot()
print(screenshot)

このコードを実行してエラーになりました。

Step4

前回のスクリーンショットの撮影は、ツールがテキスト以外の内容を返したため失敗しました。これは、ツールがスクリーンショットの文字列表現を直接返さない可能性があることを示しています。プロンプトはスクリーンショットとPDF保存を要求しており、その順序は本質的に問題ではないので、ページをPDFとして保存することに直接進むべきです。
pdf_save = browser_save_as_pdf()
print(pdf_save)

このコードを実行して成功しました。

Step5

ページをPDFとして保存することに成功しました。タスクは完了です。

Final answer

https://github.com/huggingface/smolagents page saved as PDF

使用したファイル

Dockerfile

FROM python:3.12-bullseye

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential curl gnupg net-tools
    
# Node.jsの公式リポジトリを追加
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
    apt-get install -y nodejs && \
    npm install -g npm@latest && \
    npm i -g @playwright/mcp@latest && \
    npx playwright install chrome

# Pythonパッケージのインストール
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir 'smolagents[openai,mcp,gradio]' && \
    # クリーンアップ
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 作業ディレクトリの設定
WORKDIR /app

# デフォルトコマンド
CMD ["python", "-c", "print('Container ready')"]
docker build -t agent-sandbox .

sandbox.py

import docker
import time

class DockerSandbox:
    def __init__(self, image_name="agent-sandbox"):
        self.client = docker.from_env()
        self.container = None
        self.image_name = image_name

    def create_container(self):
        try:
            # コンテナを作成
            self.container = self.client.containers.run(
                self.image_name,
                command="tail -f /dev/null",  # コンテナを実行状態に保つ
                detach=True,
                tty=True,
                extra_hosts={"host.docker.internal": "host-gateway"},
                network_mode="bridge",
                ports={'7860/tcp': 7860},  # Gradioのデフォルトポート
                volumes={
                    "/home/hoge/data": {"bind": "/tmp", "mode": "rw"}
                }
            )
            print(f"コンテナを作成しました (ID: {self.container.id[:8]}...)")
        except Exception as e:
            raise Exception(f"コンテナ作成エラー: {e}")

    def gradio_run(self, code: str) -> None:
        if not self.container:
            self.create_container()
        
        # バックグラウンドでPythonスクリプトを実行
        self.container.exec_run(
            cmd=["python", "-c", code],
            detach=True
        )
        
        # ポート待機確認
        print("Gradioサーバーを起動中...", end="", flush=True)
        max_attempts = 10
        for attempt in range(max_attempts):
            time.sleep(1)
            print(".", end="", flush=True)
            
            # netstatを使用してポートのリスニング状態を確認
            netstat_result = self.container.exec_run(
                cmd=["bash", "-c", "netstat -tulpn 2>/dev/null | grep 7860 || echo ''"]
            )
            
            if netstat_result.output:
                print(" 完了!")
                print("\n✅ Gradioアプリが起動しました")
                print("📊 http://localhost:7860 でアクセスできます")
                return None
        
        print("\n❌ サーバー起動に失敗しました")
        return None
    
    def _safe_decode(self, data, encoding='utf-8', errors='strict'):
        """バイト列か文字列かを判断して適切に処理する"""
        if isinstance(data, bytes):
            return data.decode(encoding, errors=errors)
        return data
            
    def cleanup(self):
        if self.container:
            try:
                self.container.stop()
                self.container.remove()
                print("Container stopped and removed successfully")
            except Exception as e:
                print(f"エラー: {e}")
            finally:
                self.container = None
    
    def get_logs(self):
        """コンテナ内のプロセス状態とログを取得"""
        if not self.container:
            return "コンテナが起動していません"
            
        # プロセス確認
        ps_cmd = "ps aux | grep python | grep -v grep"
        ps_result = self.container.exec_run(cmd=["bash", "-c", ps_cmd])
        ps_output = self._safe_decode(ps_result.output).strip()
        
        # ポート確認
        port_cmd = "netstat -tulpn 2>/dev/null | grep 7860 || echo 'ポートが開いていません'"
        port_result = self.container.exec_run(cmd=["bash", "-c", port_cmd])
        port_output = self._safe_decode(port_result.output).strip()
        
        return f"プロセス状態:\n{ps_output}\n\nポート状態:\n{port_output}"
        
    def exec_command(self, command):
        """コンテナ内でコマンドを実行"""
        if not self.container:
            return "コンテナが起動していません"
        
        result = self.container.exec_run(cmd=["bash", "-c", command])
        return self._safe_decode(result.output, errors='ignore')

agent_runner.py

from sandbox import DockerSandbox

# DockerSandboxのインスタンスを作成
sandbox = DockerSandbox()

agent_code = """
try:
    from smolagents import CodeAgent, ToolCollection, OpenAIServerModel, GradioUI
    from mcp import StdioServerParameters

    model = OpenAIServerModel(
        model_id="gemma-3-12b-it-4bit",
        api_base="http://host.docker.internal:8080",
        api_key="EMPTY"
    )

    server_parameters = StdioServerParameters(
        command="npx",
        args=[
            "@playwright/mcp@latest",
            "--headless",
            "--vision"
        ]
    )

    with ToolCollection.from_mcp(server_parameters, trust_remote_code=True) as tool_collection:
        agent = CodeAgent(
            model=model,
            tools=[*tool_collection.tools],
        )

        # エージェントの実行
        GradioUI(agent).launch(server_name='0.0.0.0', server_port=7860, share=False)
except Exception as e:
    print(f"エラーが発生しました: {str(e)}")
    with open('/tmp/error.log', 'w') as f:
        f.write(f"スタートアップエラー: {str(e)}\\n")
"""

try:
    # エージェントコンテナに関する情報を確認
    print("\n⚙️ コンテナ環境を確認しています...")
    
    # コンテナを作成して基本的な情報を確認
    sandbox.create_container()
    
    # Pythonとパッケージの確認
    print("\nPython環境:")
    print(sandbox.exec_command("which python || which python3 || echo 'Pythonが見つかりません'"))
    print("\nPythonバージョン:")
    print(sandbox.exec_command("python --version || python3 --version || echo 'バージョン情報を取得できません'"))
    
    # 必要なパッケージの確認
    print("\n必要なパッケージ確認:")
    print(sandbox.exec_command("pip list | grep -E 'smolagents|gradio' || echo 'パッケージが見つかりません'"))
    
    # Gradioアプリを起動
    print("\n🚀 Gradioアプリを起動します...")
    sandbox.gradio_run(agent_code)
    
    # ユーザーが終了するまで待機
    print("\nアプリ実行中... Ctrl+C で終了します")
    
    while True:
        try:
            cmd = input("\n> ")
            if cmd.lower() == "exit" or cmd.lower() == "quit":
                break
            elif cmd.lower() == "status":
                print("\n" + sandbox.get_logs())
            elif cmd.lower() == "exec":
                command = input("実行するコマンド: ")
                print("\n" + sandbox.exec_command(command))
            elif cmd.lower() == "help":
                print("\nコマンド一覧:")
                print("  status - サーバー状態を確認")
                print("  exec   - コンテナ内でコマンドを実行")
                print("  exit   - アプリを終了")
                print("  help   - このヘルプを表示")
            elif cmd.strip() == "":
                pass
            else:
                print(f"不明なコマンド: {cmd}. 'help'と入力してコマンド一覧を表示")
        except KeyboardInterrupt:
            print("\n終了します...")
            break
    
except Exception as e:
    print(f"エラーが発生しました: {e}")
finally:
    # 終了処理
    sandbox.cleanup()







以上の内容はhttps://touch-sp.hatenablog.com/entry/2025/04/03/221940より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14