はじめに
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()