browser-use を使って情シス業務を自動化するための実践的テクニック

こんにちは、CTOの小笠原(@yamitzky)です。

最近、LLMを使ってブラウザ操作を自動化する、browser-use が流行っていますね! 今回は、毎月実施している情シスタスクの一つをテーマに、browser-use で業務自動化できるかを検証してみました。

browser-use とは

browser-use は、AIエージェントを使ってブラウザ操作を自動化するツールです。ブラウザ操作には内部的には playwright を使い、AI 部分は langchain を使っています。また、タスクを遂行するためのエージェントとしての仕組み(タスクを分解して、ネクストアクションを決め、ゴールが達成できているかを評価する)も備わっています。

そのため、ざっくりと「◯◯◯のサイトを開いて、◯◯◯の予約をして」というと、ブラウザを動かしてゴールを達成してくれます。

browser-use.com

ただし、「業務を自動化する」となるといくつかハイコンテキストな部分があり、雑に指示するだけでは難しいな、という印象です。

自動化したい情シスタスクの概要

情シス部門では毎月、「アカウントの棚卸し」という業務を行っています。これは、各種SaaSサービスにログインし、アカウント一覧をCSVなどでエクスポートし、退職済みの方がいないか、使われていないライセンスがないか、などを確認する業務です。セキュリティインシデントの防止や、コスト削減のために、単純ではあるものの重要な業務です。これを解決する SaaS などもありますが、それほど時間がかかっていないというのもあり、現在は手動で行っています。

今回は、Zoomを対象に「アカウント一覧をCSVなどでエクスポート」までを自動化してみます。

Step 1:公式ドキュメントに従って browser-use を使ってみる

import asyncio

from browser_use import Agent, Controller
from browser_use.browser.browser import Browser, BrowserConfig
from langchain_openai import ChatOpenAI

browser = Browser(
    config=BrowserConfig(
        headless=False,
        disable_security=False,
    )
)
controller = Controller()
model = ChatOpenAI(model="gpt-4o")


async def main():
    task = "Zoom のユーザー一覧ページを開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください"
    agent = Agent(
        task=task, llm=model, use_vision=True, controller=controller, browser=browser
    )
    await agent.run()
    await browser.close()


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

公式ドキュメントに従えば、上記のようなコードで動きそう... ですが、実際にはうまくいきません。業務知識として「ZoomのID/PASSWORD」が必要なので、エージェントにはログインできないのです。放置していると、下記スクリーンショットのように、存在しないアカウントでログインを試みます。

Zoomのログインに失敗する様子(3倍速)

Step 2:ログインを実装する

さすがに LLM の API に ID/PASSWORD を送りたくないので、人間がログインをするようにしましょう。また、具体的にどの URL を開くのか指定した方が、結果が安定します。

# ...略...

browser = Browser(
    config=BrowserConfig(
        chrome_instance_path="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
    )
)

# ...略...

@controller.action("ユーザーにログインを依頼する")
def ask_login():
    # Macユーザー限定
    subprocess.run(["say", "-v", "Kyoko", "ログインしてください"])
    input("ログインしてください。ログインが完了したら、Enterキーを押してください")
    return ActionResult(
        extracted_content="ログインが完了しました", include_in_memory=True
    )


async def main():
    async with await browser.new_context() as context:
        task = "https://zoom.us/profile を開き、ログイン済みかを確かめてください。ログインが完了していない場合は、ユーザーにログインを依頼(ask_login)してください。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

        task = "https://zoom.us/account/user#/ を開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

    await browser.close()

一部を省略しましたが、 @controller.action("ユーザーにログインを依頼する") で tool を登録してあげると、人間に操作を依頼できるようになります。tool 名はプロンプト内で具体的に指定した方が、期待通りに動作をしてくれます。

加えて、次の3つのテクニックを追加しました。こちらは必須というよりは好みの問題です。

  • 複数の Agent に分割している
    • AI になるべく単純なタスクを与えるため。実際には、単一のAgent、単一のプロンプトでもうまくいくことが多いです。
  • Playwright の Headless Chrome ではなく、実際のブラウザを使う(上記例ではChrome Canaryを指定)
    • ログイン情報(セッション)が保存されたり、Chrome 拡張をインストールしておくことがやりやすいです。
  • say コマンドで音声を読み上げる(Macユーザー限定)
    • バックグラウンドで AI に操作させておいて、人間の対応が必要なときに気づけるようになります。

上記の対応で、ログインまではうまくいくようになります...が、実際にはエクスポートしたファイルのダウンロードができていません。

Step3:ダウンロードに対応

Step2では一見するとダウンロードが成功したように見えるのですが、Playwright の仕様でダウンロード済みのファイルにアクセスすることができません。関連した Issue も立っており、回避策が提案されています。

https://github.com/browser-use/browser-use/issues/91

事前に download イベントに対応する処理を記載する必要があります。

# ...略...

def get_or_create_download_dir() -> Path:
    exe_dir = Path(os.path.dirname(os.path.abspath(__file__)))
    download_dir = exe_dir / "downloads"
    download_dir.mkdir(exist_ok=True, parents=True)
    return download_dir

async def handle_download(download: Download):
    original_path = await download.path()
    # ファイル名からダウンロード元サイトがわかるよう、example_com 形式でドメインを追加
    domain_prefix = download.url.split("/")[2].replace(".", "_")
    new_filename = f"{domain_prefix}_{download.suggested_filename}"
    new_path = get_or_create_download_dir() / new_filename
    os.rename(original_path, new_path)

async def handle_new_page(page: PlaywrightPage):
    page.on("download", handle_download)

async def setup_download(playwright_browser: PlaywrightBrowser):
    while len(playwright_browser.contexts) < 1:
        await asyncio.sleep(1)
        print("waiting for contexts to be created")
    for context in playwright_browser.contexts:
        context.on("page", handle_new_page)
        for page in context.pages:
            page.on("download", handle_download)

async def main():
    async with await browser.new_context() as context:
        pw_browser = await context.browser.get_playwright_browser()
        asyncio.create_task(setup_download(pw_browser))

        # ...略...

これで、基本的には問題なく動くと思います!

browser-useでエクスポートが成功している例(3倍速)

Step4:AI に処理の完了をちゃんと確認させる(おまけ)

今回の Zoom のエクスポートはかなり単純な例でした。他の SaaS では、エクスポートが非同期で行われることも多いです。複雑な処理の場合、Agent は誤って「ダウンロードが完了した」と誤解してしまうことがあります。

そこで、Agent にダウンロード済みファイルの確認をさせます。

# ...略...

@controller.action("ダウンロード済みファイル一覧を確認する")
def get_downloaded_files():
    download_dir = get_or_create_download_dir()
    files = list(download_dir.glob("*"))
    if not files:
        return ActionResult(
            extracted_content="ダウンロードフォルダに現在ファイルはありません。",
            include_in_memory=True,
        )

    file_list = "\n".join([f"- {file.name}" for file in files])
    return ActionResult(
        extracted_content=f"ダウンロードフォルダの内容:\n{file_list}",
        include_in_memory=True,
    )


async def main():
    async with await browser.new_context() as context:
        pw_browser = await context.browser.get_playwright_browser()
        asyncio.create_task(setup_download(pw_browser))

        task = "https://zoom.us/profile を開き、ログイン済みかを確かめてください。ログインが完了していない場合は、ユーザーにログインを依頼(ask_login)してください。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

        task = "https://zoom.us/account/user#/ を開き、エクスポート → テーブル内のすべてのユーザー からダウンロードをしてください。作業を完了する前に、ダウンロードフォルダのファイル一覧(get_downloaded_files)を確認し、ダウンロードができているかを確認してください。ファイル名にはダウンロード元サイトのドメインが含まれています。"
        agent = Agent(
            task=task,
            llm=model,
            use_vision=True,
            controller=controller,
            browser_context=context,
        )
        await agent.run()

# ...略...

「ダウンロード済みファイル一覧を確認する」という tool を作成し、それによって作業の完了を確認させるようにしました。

実際、意地悪をして get_downloaded_files 関数が常に「ダウンロードフォルダに現在ファイルはありません。」を返すようにすると、延々とダウンロード処理を繰り返します。

振り返り

今回は「Zoom のユーザー一覧をエクスポートする」という作業をテーマに、browser-use での自動化をしてみました。

実際には他にも情シス業務の自動化を検証してみていますが、ハードルはやや高いかなという印象です。例えば、Google Sheets (スプレッドシート) のような複雑な UI の操作はあまりうまくいかない印象で、tool を用意してあげたり、タスクを分解してあげる、人間のレビューを挟むなど、「AI に期待しすぎない」というのがコツかなと思いました。

一方で、「AIでもわかるようにドキュメントや手順を簡略化する」という検討も進むので、良い機会にはなりました!

ぜひ皆さんも業務自動化をチャレンジしてみてください!