From b4a47169a9c667149cf2ac595dc3cb57ce9ecf58 Mon Sep 17 00:00:00 2001
From: Haochen Zhang <haochenz@umich.edu>
Date: Thu, 13 Feb 2025 09:39:24 -0800
Subject: [PATCH] Fix: Change Playwright browser tool from sync to async
 (#17808)

---
 .../llama-index-tools-playwright/README.md    |  33 +-
 .../examples/BUILD                            |   1 -
 .../examples/playwright_browser_agent.ipynb   | 449 ++++++++++++++++++
 .../examples/web_browsing.py                  |  42 --
 .../llama_index/tools/playwright/base.py      | 158 +++---
 .../pyproject.toml                            |   2 +-
 .../tests/test_tools_playwright.py            |  41 +-
 7 files changed, 580 insertions(+), 146 deletions(-)
 delete mode 100644 llama-index-integrations/tools/llama-index-tools-playwright/examples/BUILD
 create mode 100644 llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb
 delete mode 100644 llama-index-integrations/tools/llama-index-tools-playwright/examples/web_browsing.py

diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/README.md b/llama-index-integrations/tools/llama-index-tools-playwright/README.md
index 2d9fd3e74e..705ed7d645 100644
--- a/llama-index-integrations/tools/llama-index-tools-playwright/README.md
+++ b/llama-index-integrations/tools/llama-index-tools-playwright/README.md
@@ -2,6 +2,9 @@
 
 This tool is a wrapper around the Playwright library. It allows you to navigate to a website, extract text and hyperlinks, and click on elements.
 
+> **Warning**
+> Only support async functions and playwright browser APIs.
+
 ## Installation
 
 ```
@@ -10,11 +13,11 @@ pip install llama-index-tools-playwright
 
 ## Setup
 
-In order to use this tool, you need to have a sync Playwright browser instance. You can hook one up by running the following code:
+In order to use this tool, you need to have a async Playwright browser instance. You can hook one up by running the following code:
 
 ```python
-browser = PlaywrightToolSpec.create_sync_playwright_browser(headless=False)
-playwright_tool = PlaywrightToolSpec.from_sync_browser(browser)
+browser = PlaywrightToolSpec.create_async_playwright_browser(headless=False)
+playwright_tool = PlaywrightToolSpec.from_async_browser(browser)
 ```
 
 ## Usage
@@ -22,31 +25,37 @@ playwright_tool = PlaywrightToolSpec.from_sync_browser(browser)
 ### Navigate to a website
 
 ```python
-playwright_tool.navigate_to("https://playwright.dev/python/docs/intro")
+await playwright_tool.navigate_to("https://playwright.dev/python/docs/intro")
+```
+
+### Navigate back
+
+```python
+await playwright_tool.navigate_back()
 ```
 
 ### Get current page URL
 
 ```python
-playwright_tool.get_current_page()
+await playwright_tool.get_current_page()
 ```
 
 ### Extract all hyperlinks
 
 ```python
-playwright_tool.extract_hyperlinks()
+await playwright_tool.extract_hyperlinks()
 ```
 
 ### Extract all text
 
 ```python
-playwright_tool.extract_text()
+await playwright_tool.extract_text()
 ```
 
 ### Get element attributes
 
 ```python
-element = playwright_tool.get_elements(
+element = await playwright_tool.get_elements(
     selector="ELEMENT_SELECTOR", attributes=["innerText"]
 )
 ```
@@ -54,11 +63,15 @@ element = playwright_tool.get_elements(
 ### Click on an element
 
 ```python
-playwright_tool.click(selector="ELEMENT_SELECTOR")
+await playwright_tool.click(selector="ELEMENT_SELECTOR")
 ```
 
 ### Fill in an input field
 
 ```python
-playwright_tool.fill(selector="ELEMENT_SELECTOR", value="Hello")
+await playwright_tool.fill(selector="ELEMENT_SELECTOR", value="Hello")
 ```
+
+## Agentic Usage
+
+This tool has a more extensive example usage documented in a Jupyter notebook [here](https://github.com/run-llama/llama_index/blob/main/llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb)
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/examples/BUILD b/llama-index-integrations/tools/llama-index-tools-playwright/examples/BUILD
deleted file mode 100644
index db46e8d6c9..0000000000
--- a/llama-index-integrations/tools/llama-index-tools-playwright/examples/BUILD
+++ /dev/null
@@ -1 +0,0 @@
-python_sources()
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb b/llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb
new file mode 100644
index 0000000000..8e8f4b92b3
--- /dev/null
+++ b/llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb
@@ -0,0 +1,449 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Building a Playwright Browser Agent\n",
+    "\n",
+    "<a href=\"https://colab.research.google.com/github/run-llama/llama_index/blob/main/llama-index-integrations/tools/llama-index-tools-playwright/examples/playwright_browser_agent.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
+    "\n",
+    "This tutorial walks through using the LLM tools provided by the [Playwright](https://playwright.dev/) to allow LLMs to easily navigate and scrape content from the Internet."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Instaniation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%pip install llama-index-tools-playwright llama-index"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# set up async playwright browser\n",
+    "# To enable more llamaindex usecases, we only offer async playwright tools at the moment\n",
+    "\n",
+    "# install playwright\n",
+    "!playwright install\n",
+    "\n",
+    "# This import is required only for jupyter notebooks, since they have their own eventloop\n",
+    "import nest_asyncio\n",
+    "\n",
+    "nest_asyncio.apply()\n",
+    "\n",
+    "# import the tools\n",
+    "from llama_index.tools.playwright.base import PlaywrightToolSpec\n",
+    "\n",
+    "# create the tools\n",
+    "browser = await PlaywrightToolSpec.create_async_playwright_browser(headless=True)\n",
+    "playwright_tool = PlaywrightToolSpec.from_async_browser(browser)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Testing the playwright tools"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Listing all tools"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "click\n",
+      "fill\n",
+      "get_current_page\n",
+      "extract_hyperlinks\n",
+      "extract_text\n",
+      "get_elements\n",
+      "navigate_to\n",
+      "navigate_back\n"
+     ]
+    }
+   ],
+   "source": [
+    "playwright_tool_list = playwright_tool.to_tool_list()\n",
+    "for tool in playwright_tool_list:\n",
+    "    print(tool.metadata.name)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Navigating to playwright doc website"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "https://playwright.dev/python/docs/intro\n"
+     ]
+    }
+   ],
+   "source": [
+    "await playwright_tool.navigate_to(\"https://playwright.dev/python/docs/intro\")\n",
+    "\n",
+    "### Print the current page URL\n",
+    "print(await playwright_tool.get_current_page())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Extract all hyperlinks"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[\"/python/docs/actionability\", \"#introduction\", \"/python/docs/webview2\", \"/python/docs/dialogs\", \"/python/docs/api-testing\", \"/python/docs/navigations\", \"/python/docs/pom\", \"https://www.youtube.com/channel/UC46Zj8pDH5tDosqm1gd7WTg\", \"/python/docs/aria-snapshots\", \"#\", \"/python/docs/trace-viewer\", \"/python/\", \"/python/docs/handles\", \"/python/docs/input\", \"https://pypi.org/project/pytest-playwright/\", \"/python/docs/locators\", \"/dotnet/docs/intro\", \"/python/docs/codegen-intro\", \"/python/docs/auth\", \"/python/docs/browser-contexts\", \"/python/docs/other-locators\", \"https://www.linkedin.com/company/playwrightweb\", \"/python/docs/downloads\", \"https://twitter.com/playwrightweb\", \"/python/docs/intro\", \"/python/docs/intro#running-the-example-test\", \"/python/docs/frames\", \"/python/docs/release-notes\", \"#running-the-example-test\", \"#system-requirements\", \"/python/docs/evaluating\", \"/python/docs/writing-tests\", \"/python/docs/network\", \"/python/docs/screenshots\", \"/docs/intro\", \"/python/docs/videos\", \"/python/docs/languages\", \"https://learn.microsoft.com/en-us/training/modules/build-with-playwright/\", \"/python/docs/emulation\", \"https://dev.to/playwright\", \"https://aka.ms/playwright/discord\", \"/python/docs/test-assertions\", \"#whats-next\", \"/python/docs/api/class-playwright\", \"/python/docs/debug\", \"/java/docs/intro\", \"/python/docs/events\", \"#installing-playwright-pytest\", \"/python/docs/running-tests\", \"/python/docs/pages\", \"/python/docs/browsers\", \"https://github.com/microsoft/playwright-python\", \"/python/community/ambassadors\", \"/python/docs/mock\", \"/python/docs/chrome-extensions\", \"/python/docs/clock\", \"/python/community/learn-videos\", \"/python/docs/library\", \"/python/community/welcome\", \"/python/docs/trace-viewer-intro\", \"#updating-playwright\", \"https://stackoverflow.com/questions/tagged/playwright\", \"/python/docs/intro#installing-playwright-pytest\", \"/python/docs/extensibility\", \"#add-example-test\", \"#__docusaurus_skipToContent_fallback\", \"/python/docs/test-runners\", \"/python/docs/codegen\", \"https://anaconda.org/Microsoft/pytest-playwright\", \"/python/docs/ci-intro\", \"/python/community/feature-videos\"]\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(await playwright_tool.extract_hyperlinks())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Extract all text"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Installation | Playwright Python Skip to main content Playwright for Python Docs API Python Python Node.js Java .NET Community Search ⌘ K Getting Started Installation Writing tests Generating tests Running and debugging tests Trace viewer Setting up CI Pytest Plugin Reference Getting started - Library Release notes Guides Actions Auto-waiting API testing Assertions Authentication Browsers Chrome extensions Clock Debugging Tests Dialogs Downloads Emulation Evaluating JavaScript Events Extensibility Frames Handles Isolation Locators Mock APIs Navigations Network Other locators Pages Page object models Screenshots Snapshot testing Test generator Trace viewer Videos WebView2 Integrations Supported languages Getting Started Installation On this page Installation Introduction ​ Playwright was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with native mobile emulation. The Playwright library can be used as a general purpose browser automation tool, providing a powerful set of APIs to automate web applications, for both sync and async Python. This introduction describes the Playwright Pytest plugin, which is the recommended way to write end-to-end tests. You will learn How to install Playwright Pytest How to run the example test Installing Playwright Pytest ​ Playwright recommends using the official Playwright Pytest plugin to write end-to-end tests. It provides context isolation, running it on multiple browser configurations out of the box. Get started by installing Playwright and running the example test to see it in action. PyPI Anaconda Install the Pytest plugin : pip install pytest-playwright Install the Pytest plugin : conda config --add channels conda-forge conda config --add channels microsoft conda install pytest-playwright Install the required browsers: playwright install Add Example Test ​ Create a file that follows the test_ prefix convention, such as test_example.py , inside the current working directory or in a sub-directory with the code below. Make sure your test name also follows the test_ prefix convention. test_example.py import re from playwright . sync_api import Page , expect def test_has_title ( page : Page ) : page . goto ( \"https://playwright.dev/\" ) # Expect a title \"to contain\" a substring. expect ( page ) . to_have_title ( re . compile ( \"Playwright\" ) ) def test_get_started_link ( page : Page ) : page . goto ( \"https://playwright.dev/\" ) # Click the get started link. page . get_by_role ( \"link\" , name = \"Get started\" ) . click ( ) # Expects page to have a heading with the name of Installation. expect ( page . get_by_role ( \"heading\" , name = \"Installation\" ) ) . to_be_visible ( ) Running the Example Test ​ By default tests will be run on chromium. This can be configured via the CLI options . Tests are run in headless mode meaning no browser UI will open up when running the tests. Results of the tests and test logs will be shown in the terminal. pytest Updating Playwright ​ To update Playwright to the latest version run the following command: pip install pytest-playwright playwright -U System requirements ​ Python 3.8 or higher. Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). macOS 13 Ventura, or later. Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. What's next ​ Write tests using web first assertions, page fixtures and locators Run single test, multiple tests, headed mode Generate tests with Codegen See a trace of your tests Next Writing tests Introduction Installing Playwright Pytest Add Example Test Running the Example Test Updating Playwright System requirements What's next Learn Getting started Playwright Training Learn Videos Feature Videos Community Stack Overflow Discord Twitter LinkedIn More GitHub YouTube Blog Ambassadors Copyright © 2025 Microsoft\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(await playwright_tool.extract_text())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Get element\n",
+    "Get element attributes for navigating to the next page.\n",
+    "You can retrieve the selector from google chrome dev tools."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[{\"innerText\": \"Next\\nWriting tests\"}]\n"
+     ]
+    }
+   ],
+   "source": [
+    "element = await playwright_tool.get_elements(\n",
+    "    selector=\"#__docusaurus_skipToContent_fallback > div > div > main > div > div > div.col.docItemCol_VOVn > div > nav > a\",\n",
+    "    attributes=[\"innerText\"],\n",
+    ")\n",
+    "print(element)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Click\n",
+    "Click on the search bar"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "\"Clicked element '#__docusaurus > nav > div.navbar__inner > div.navbar__items.navbar__items--right > div.navbarSearchContainer_Bca1 > button'\""
+      ]
+     },
+     "execution_count": null,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "await playwright_tool.click(\n",
+    "    selector=\"#__docusaurus > nav > div.navbar__inner > div.navbar__items.navbar__items--right > div.navbarSearchContainer_Bca1 > button\"\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Fill\n",
+    "Fill in the search bar with \"Mouse click\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "\"Filled element '#docsearch-input'\""
+      ]
+     },
+     "execution_count": null,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "await playwright_tool.fill(selector=\"#docsearch-input\", value=\"Mouse click\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Click on the first result, we should be redirected to the Mouse click page"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "https://playwright.dev/python/docs/input#mouse-click\n"
+     ]
+    }
+   ],
+   "source": [
+    "await playwright_tool.click(selector=\"#docsearch-hits0-item-0\")\n",
+    "print(await playwright_tool.get_current_page())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Using the playwright tool with agent\n",
+    "To get started, you will need an [OpenAI api key](https://platform.openai.com/account/api-keys)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# set your openai key, if using openai\n",
+    "import os\n",
+    "\n",
+    "os.environ[\"OPENAI_API_KEY\"] = \"YOUR_OPENAI_API_KEY\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from llama_index.core.agent import FunctionCallingAgent\n",
+    "from llama_index.llms.openai import OpenAI\n",
+    "\n",
+    "playwright_tool_list = playwright_tool.to_tool_list()\n",
+    "\n",
+    "agent = FunctionCallingAgent.from_tools(\n",
+    "    playwright_tool_list,\n",
+    "    llm=OpenAI(model=\"gpt-4o\"),\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Sam Altman's blog post on productivity offers a comprehensive guide to enhancing personal efficiency and effectiveness. Here are the key points:\n",
+      "\n",
+      "1. **Compound Growth**: Altman emphasizes the importance of small productivity gains compounded over time, likening it to financial growth.\n",
+      "\n",
+      "2. **Choosing the Right Work**: The most crucial aspect of productivity is selecting the right tasks. Independent thought and strong personal beliefs are vital.\n",
+      "\n",
+      "3. **Delegation and Enjoyment**: Delegate tasks based on others' strengths and interests. Avoid work that doesn't interest you, as it hampers morale and productivity.\n",
+      "\n",
+      "4. **Learning and Collaboration**: Embrace the ability to learn quickly and surround yourself with inspiring, positive people.\n",
+      "\n",
+      "5. **Prioritization**: Altman uses lists to manage tasks and prioritizes work that builds momentum. He stresses the importance of saying no to non-critical tasks.\n",
+      "\n",
+      "6. **Time Management**: Avoid unnecessary meetings and optimize your schedule for productivity, leaving room for serendipitous encounters.\n",
+      "\n",
+      "7. **Physical Well-being**: Sleep, exercise, and nutrition are critical. Altman shares his personal routines, including sleep optimization, exercise, and dietary habits.\n",
+      "\n",
+      "8. **Workspace and Tools**: A conducive workspace and efficient tools, like custom software and fast typing skills, enhance productivity.\n",
+      "\n",
+      "9. **Overcommitment**: A slight overcommitment can boost efficiency, but excessive overcommitment is detrimental.\n",
+      "\n",
+      "10. **Direction Matters**: Productivity is meaningless if directed towards the wrong goals. Focus on what truly matters.\n",
+      "\n",
+      "Altman concludes by emphasizing the importance of balancing productivity with personal happiness and relationships.\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\n",
+    "    agent.chat(\n",
+    "        \"Navigate to https://blog.samaltman.com/productivity, extract the text on this page and return a summary of the article.\"\n",
+    "    )\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Using the playwright tool with agent workflow"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from llama_index.llms.openai import OpenAI\n",
+    "from llama_index.core.agent.workflow import AgentWorkflow\n",
+    "\n",
+    "from llama_index.core.agent.workflow import (\n",
+    "    AgentInput,\n",
+    "    AgentOutput,\n",
+    "    ToolCall,\n",
+    "    ToolCallResult,\n",
+    "    AgentStream,\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "llm = OpenAI(model=\"gpt-4o\")\n",
+    "\n",
+    "workflow = AgentWorkflow.from_tools_or_functions(\n",
+    "    playwright_tool_list,\n",
+    "    llm=llm,\n",
+    "    system_prompt=\"You are a helpful assistant that can do browser automation and data extraction\",\n",
+    ")\n",
+    "\n",
+    "handler = workflow.run(\n",
+    "    user_msg=\"Navigate to https://blog.samaltman.com/productivity, extract the text on this page and return a summary of the article.\"\n",
+    ")\n",
+    "\n",
+    "async for event in handler.stream_events():\n",
+    "    if isinstance(event, AgentStream):\n",
+    "        print(event.delta, end=\"\", flush=True)\n",
+    "        # print(event.response)  # the current full response\n",
+    "        # print(event.raw)  # the raw llm api response\n",
+    "        # print(event.current_agent_name)  # the current agent name\n",
+    "    # elif isinstance(event, AgentInput):\n",
+    "    # print(event.input)  # the current input messages\n",
+    "    # print(event.current_agent_name)  # the current agent name\n",
+    "    # elif isinstance(event, AgentOutput):\n",
+    "    # print(event.response)  # the current full response\n",
+    "    # print(event.tool_calls)  # the selected tool calls, if any\n",
+    "    # print(event.raw)  # the raw llm api response\n",
+    "    elif isinstance(event, ToolCallResult):\n",
+    "        print(event.tool_name)  # the tool name\n",
+    "        print(event.tool_kwargs)  # the tool kwargs\n",
+    "        print(event.tool_output)  # the tool output\n",
+    "    # elif isinstance(event, ToolCall):\n",
+    "    # print(event.tool_name)  # the tool name\n",
+    "    # print(event.tool_kwargs)  # the tool kwargs"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "llama-index-zpEnpL0o-py3.12",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/examples/web_browsing.py b/llama-index-integrations/tools/llama-index-tools-playwright/examples/web_browsing.py
deleted file mode 100644
index 547b49db55..0000000000
--- a/llama-index-integrations/tools/llama-index-tools-playwright/examples/web_browsing.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from llama_index.tools.playwright.base import PlaywrightToolSpec
-import time
-
-# Create Playwright browser and tool object
-browser = PlaywrightToolSpec.create_sync_playwright_browser(headless=False)
-playwright_tool = PlaywrightToolSpec.from_sync_browser(browser)
-
-# List all tools
-playwright_tool_list = playwright_tool.to_tool_list()
-for tool in playwright_tool_list:
-    print(tool.metadata.name)
-
-# Navigate to the playwright doc website
-playwright_tool.navigate_to("https://playwright.dev/python/docs/intro")
-time.sleep(1)
-
-# Print the current page URL
-print(playwright_tool.get_current_page())
-
-# Extract all hyperlinks
-print(playwright_tool.extract_hyperlinks())
-
-# Extract all text
-print(playwright_tool.extract_text())
-
-# Get element attributes for navigating to the next page
-# You can retrieve the selector from google chrome dev tools
-element = playwright_tool.get_elements(
-    selector="#__docusaurus_skipToContent_fallback > div > div > main > div > div > div.col.docItemCol_VOVn > div > nav > a",
-    attributes=["innerText"],
-)
-print(element)
-
-# Click on the search bar
-playwright_tool.click(
-    selector="#__docusaurus > nav > div.navbar__inner > div.navbar__items.navbar__items--right > div.navbarSearchContainer_Bca1 > button"
-)
-time.sleep(1)
-
-# Fill in the search bar
-playwright_tool.fill(selector="#docsearch-input", value="Hello")
-time.sleep(1)
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/llama_index/tools/playwright/base.py b/llama-index-integrations/tools/llama-index-tools-playwright/llama_index/tools/playwright/base.py
index cf5cfdc18f..14306bd510 100644
--- a/llama-index-integrations/tools/llama-index-tools-playwright/llama_index/tools/playwright/base.py
+++ b/llama-index-integrations/tools/llama-index-tools-playwright/llama_index/tools/playwright/base.py
@@ -3,8 +3,8 @@ import json
 from urllib.parse import urlparse, urljoin
 from bs4 import BeautifulSoup
 
-from playwright.sync_api import Browser as SyncBrowser
-from playwright.sync_api import Page as SyncPage
+from playwright.async_api import Browser as AsyncBrowser
+from playwright.async_api import Page as AsyncPage
 
 from llama_index.core.tools.tool_spec.base import BaseToolSpec
 
@@ -16,6 +16,7 @@ class PlaywrightToolSpec(BaseToolSpec):
 
     spec_functions = [
         "click",
+        "fill",
         "get_current_page",
         "extract_hyperlinks",
         "extract_text",
@@ -26,8 +27,8 @@ class PlaywrightToolSpec(BaseToolSpec):
 
     def __init__(
         self,
-        sync_browser: Optional[SyncBrowser] = None,
-        visible_only: bool = True,
+        async_browser: Optional[AsyncBrowser] = None,
+        visible_only: bool = False,
         playwright_strict: bool = False,
         playwright_timeout: float = 1_000,
         absolute_url: bool = False,
@@ -37,7 +38,7 @@ class PlaywrightToolSpec(BaseToolSpec):
         Initialize PlaywrightToolSpec.
 
         Args:
-            sync_browser: Optional[SyncBrowser] = None. A browser instance to use for automation.
+            async_browser: Optional[AsyncBrowser] = None. A browser instance to use for automation.
             visible_only: bool = True. Whether to only click on visible elements.
             playwright_strict: bool = False. Whether to use strict mode for playwright.
             playwright_timeout: float = 1_000. Timeout for playwright operations.
@@ -45,7 +46,7 @@ class PlaywrightToolSpec(BaseToolSpec):
             html_parser: str = "html.parser". The html parser to use with BeautifulSoup
 
         """
-        self.sync_browser = sync_browser
+        self.async_browser = async_browser
 
         # for click tool
         self.visible_only = visible_only
@@ -57,11 +58,11 @@ class PlaywrightToolSpec(BaseToolSpec):
         self.html_parser = html_parser
 
     @classmethod
-    def from_sync_browser(cls, sync_browser: SyncBrowser) -> "PlaywrightToolSpec":
+    def from_async_browser(cls, async_browser: AsyncBrowser) -> "PlaywrightToolSpec":
         """
-        Initialize PlaywrightToolSpec from a sync browser instance.
+        Initialize PlaywrightToolSpec from an async browser instance.
         """
-        return cls(sync_browser=sync_browser)
+        return cls(async_browser=async_browser)
 
     #################
     # Utils Methods #
@@ -75,68 +76,68 @@ class PlaywrightToolSpec(BaseToolSpec):
         return f"{selector} >> visible=1"
 
     @staticmethod
-    def create_sync_playwright_browser(
+    async def create_async_playwright_browser(
         headless: bool = True, args: Optional[List[str]] = None
-    ) -> SyncBrowser:
+    ) -> AsyncBrowser:
         """
-        Create a playwright browser.
+        Create an async playwright browser.
 
         Args:
             headless: Whether to run the browser in headless mode. Defaults to True.
             args: arguments to pass to browser.chromium.launch
 
         Returns:
-            SyncBrowser: The playwright browser.
+            AsyncBrowser: The playwright browser.
         """
-        from playwright.sync_api import sync_playwright
+        from playwright.async_api import async_playwright
 
-        browser = sync_playwright().start()
-        return browser.chromium.launch(headless=headless, args=args)
+        browser = await async_playwright().start()
+        return await browser.chromium.launch(headless=headless, args=args)
 
-    def _get_current_page(self, browser: SyncBrowser) -> SyncPage:
+    async def _aget_current_page(self, browser: AsyncBrowser) -> AsyncPage:
         """
-        Get the current page of the browser.
+        Get the current page of the async browser.
 
         Args:
             browser: The browser to get the current page from.
 
         Returns:
-            SyncPage: The current page.
+            AsyncPage: The current page.
         """
         if not browser.contexts:
-            context = browser.new_context()
-            return context.new_page()
+            context = await browser.new_context()
+            return await context.new_page()
         context = browser.contexts[
             0
         ]  # Assuming you're using the default browser context
         if not context.pages:
-            return context.new_page()
+            return await context.new_page()
         # Assuming the last page in the list is the active one
         return context.pages[-1]
 
     #################
     # Click #
     #################
-    def click(
+    async def click(
         self,
         selector: str,
     ) -> str:
         """
-        Click on a en element based on a CSS selector.
+        Click on a web element based on a CSS selector.
 
         Args:
-            selector: The CSS selector to click on.
+            selector: The CSS selector for the web element to click on.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
 
-        page = self._get_current_page(self.sync_browser)
+        page = await self._aget_current_page(self.async_browser)
         # Navigate to the desired webpage before using this tool
         selector_effective = self._selector_effective(selector=selector)
-        from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
+        from playwright.async_api import TimeoutError as PlaywrightTimeoutError
 
         try:
-            page.click(
+            await page.click(
                 selector_effective,
                 strict=self.playwright_strict,
                 timeout=self.playwright_timeout,
@@ -148,28 +149,28 @@ class PlaywrightToolSpec(BaseToolSpec):
     #################
     # Fill #
     #################
-    def fill(
+    async def fill(
         self,
         selector: str,
         value: str,
     ) -> str:
         """
-        Fill an input field with the given value.
+        Fill an web input field specified by the given CSS selector with the given value.
 
         Args:
-            selector: The CSS selector to fill.
+            selector: The CSS selector for the web input field to fill.
             value: The value to fill in.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
 
-        page = self._get_current_page(self.sync_browser)
+        page = await self._aget_current_page(self.async_browser)
         # Navigate to the desired webpage before using this tool
         selector_effective = self._selector_effective(selector=selector)
-        from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
+        from playwright.async_api import TimeoutError as PlaywrightTimeoutError
 
         try:
-            page.fill(
+            await page.fill(
                 selector_effective,
                 value,
                 strict=self.playwright_strict,
@@ -182,13 +183,13 @@ class PlaywrightToolSpec(BaseToolSpec):
     #################
     # Get Current Page #
     #################
-    def get_current_page(self) -> str:
+    async def get_current_page(self) -> str:
         """
-        Get the url of the current page.
+        Get the url of the current web page.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
-        page = self._get_current_page(self.sync_browser)
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
+        page = await self._aget_current_page(self.async_browser)
         return page.url
 
     #################
@@ -209,29 +210,29 @@ class PlaywrightToolSpec(BaseToolSpec):
         # only appears once in the list
         return json.dumps(list(set(links)))
 
-    def extract_hyperlinks(self) -> str:
+    async def extract_hyperlinks(self) -> str:
         """
-        Extract all hyperlinks from the current page.
+        Extract all hyperlinks from the current web page.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
 
-        page = self._get_current_page(self.sync_browser)
-        html_content = page.content()
+        page = await self._aget_current_page(self.async_browser)
+        html_content = await page.content()
         return self.scrape_page(page, html_content, self.absolute_url)
 
     #################
     # Extract Text #
     #################
-    def extract_text(self) -> str:
+    async def extract_text(self) -> str:
         """
-        Extract all text from the current page.
+        Extract all text from the current web page.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
 
-        page = self._get_current_page(self.sync_browser)
-        html_content = page.content()
+        page = await self._aget_current_page(self.async_browser)
+        html_content = await page.content()
 
         # Parse the HTML content with BeautifulSoup
         soup = BeautifulSoup(html_content, self.html_parser)
@@ -241,26 +242,28 @@ class PlaywrightToolSpec(BaseToolSpec):
     #################
     # Get Elements #
     #################
-    def _get_elements(
-        self, page: SyncPage, selector: str, attributes: Sequence[str]
+    async def _aget_elements(
+        self, page: AsyncPage, selector: str, attributes: Sequence[str]
     ) -> List[dict]:
         """Get elements matching the given CSS selector."""
-        elements = page.query_selector_all(selector)
+        elements = await page.query_selector_all(selector)
         results = []
         for element in elements:
             result = {}
             for attribute in attributes:
                 if attribute == "innerText":
-                    val: Optional[str] = element.inner_text()
+                    val: Optional[str] = await element.inner_text()
                 else:
-                    val = element.get_attribute(attribute)
+                    val = await element.get_attribute(attribute)
                 if val is not None and val.strip() != "":
                     result[attribute] = val
             if result:
                 results.append(result)
         return results
 
-    def get_elements(self, selector: str, attributes: List[str] = ["innerText"]) -> str:
+    async def get_elements(
+        self, selector: str, attributes: List[str] = ["innerText"]
+    ) -> str:
         """
         Retrieve elements in the current web page matching the given CSS selector.
 
@@ -268,11 +271,11 @@ class PlaywrightToolSpec(BaseToolSpec):
             selector: CSS selector, such as '*', 'div', 'p', 'a', #id, .classname
             attribute: Set of attributes to retrieve for each element
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
 
-        page = self._get_current_page(self.sync_browser)
-        results = self._get_elements(page, selector, attributes)
+        page = await self._aget_current_page(self.async_browser)
+        results = await self._aget_elements(page, selector, attributes)
         return json.dumps(results, ensure_ascii=False)
 
     #################
@@ -286,33 +289,36 @@ class PlaywrightToolSpec(BaseToolSpec):
         if parsed_url.scheme not in ("http", "https"):
             raise ValueError("URL scheme must be 'http' or 'https'")
 
-    def navigate_to(
+    async def navigate_to(
         self,
         url: str,
     ) -> str:
         """
         Navigate to the given url.
+
+        Args:
+            url: The url to navigate to.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
         self.validate_url(url)
 
-        page = self._get_current_page(self.sync_browser)
-        response = page.goto(url)
+        page = await self._aget_current_page(self.async_browser)
+        response = await page.goto(url)
         status = response.status if response else "unknown"
         return f"Navigating to {url} returned status code {status}"
 
     #################
     # Navigate Back #
     #################
-    def navigate_back(self) -> str:
+    async def navigate_back(self) -> str:
         """
-        Navigate back to the previous page.
+        Navigate back to the previous web page.
         """
-        if self.sync_browser is None:
-            raise ValueError("Sync browser is not initialized")
-        page = self._get_current_page(self.sync_browser)
-        response = page.go_back()
+        if self.async_browser is None:
+            raise ValueError("Async browser is not initialized")
+        page = await self._aget_current_page(self.async_browser)
+        response = await page.go_back()
 
         if response:
             return (
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/pyproject.toml b/llama-index-integrations/tools/llama-index-tools-playwright/pyproject.toml
index e063eba58f..4115a2813b 100644
--- a/llama-index-integrations/tools/llama-index-tools-playwright/pyproject.toml
+++ b/llama-index-integrations/tools/llama-index-tools-playwright/pyproject.toml
@@ -28,7 +28,7 @@ license = "MIT"
 name = "llama-index-tools-playwright"
 packages = [{include = "llama_index/"}]
 readme = "README.md"
-version = "0.1.0"
+version = "0.2.0"
 
 [tool.poetry.dependencies]
 python = ">=3.9,<4.0"
diff --git a/llama-index-integrations/tools/llama-index-tools-playwright/tests/test_tools_playwright.py b/llama-index-integrations/tools/llama-index-tools-playwright/tests/test_tools_playwright.py
index e09c77c1ec..0c049d2dfc 100644
--- a/llama-index-integrations/tools/llama-index-tools-playwright/tests/test_tools_playwright.py
+++ b/llama-index-integrations/tools/llama-index-tools-playwright/tests/test_tools_playwright.py
@@ -1,6 +1,7 @@
 import pytest
 import json
 import os
+import asyncio
 
 from llama_index.core.tools.tool_spec.base import BaseToolSpec
 from llama_index.tools.playwright import PlaywrightToolSpec
@@ -94,38 +95,46 @@ TEST_SELECTOR_FILL = "#__docusaurus > nav > div.navbar__inner > div.navbar__item
 TEST_VALUE = "click"
 
 
-def test_class():
-    names_of_base_classes = [b.__name__ for b in PlaywrightToolSpec.__mro__]
-    assert BaseToolSpec.__name__ in names_of_base_classes
-
-
 @pytest.fixture(scope="session")
 def PlaywrightTool():
-    browser = PlaywrightToolSpec.create_sync_playwright_browser(headless=True)
-    playwright_tool = PlaywrightToolSpec.from_sync_browser(browser)
+    browser = asyncio.get_event_loop().run_until_complete(
+        PlaywrightToolSpec.create_async_playwright_browser(headless=True)
+    )
+    playwright_tool = PlaywrightToolSpec.from_async_browser(browser)
     yield playwright_tool
-    browser.close()
+    asyncio.get_event_loop().run_until_complete(browser.close())
+
+
+def test_class(PlaywrightTool):
+    names_of_base_classes = [b.__name__ for b in PlaywrightToolSpec.__mro__]
+    assert BaseToolSpec.__name__ in names_of_base_classes
 
 
 def test_navigate_to(PlaywrightTool):
-    PlaywrightTool.navigate_to("https://playwright.dev/python/docs/intro")
-    assert (
-        PlaywrightTool.get_current_page() == "https://playwright.dev/python/docs/intro"
+    asyncio.get_event_loop().run_until_complete(
+        PlaywrightTool.navigate_to("https://playwright.dev/python/docs/intro")
     )
+    current_page = asyncio.get_event_loop().run_until_complete(
+        PlaywrightTool.get_current_page()
+    )
+    assert current_page == "https://playwright.dev/python/docs/intro"
 
 
 def test_extract_hyperlinks(PlaywrightTool):
-    assert set(json.loads(PlaywrightTool.extract_hyperlinks())) == TEST_HYPERLINKS
+    hyperlinks = asyncio.get_event_loop().run_until_complete(
+        PlaywrightTool.extract_hyperlinks()
+    )
+    assert set(json.loads(hyperlinks)) == TEST_HYPERLINKS
 
 
 def test_extract_text(PlaywrightTool):
-    print(PlaywrightTool.extract_text())
+    text = asyncio.get_event_loop().run_until_complete(PlaywrightTool.extract_text())
     # different systems may have different whitespace, so we allow for some leeway
-    assert abs(len(PlaywrightTool.extract_text()) - len(TEST_TEXT)) < 25
+    assert abs(len(text) - len(TEST_TEXT)) < 25
 
 
 def test_get_elements(PlaywrightTool):
-    element = PlaywrightTool.get_elements(
-        selector=TEST_SELECTOR, attributes=["innerText"]
+    element = asyncio.get_event_loop().run_until_complete(
+        PlaywrightTool.get_elements(selector=TEST_SELECTOR, attributes=["innerText"])
     )
     assert element == TEST_ELEMENTS
-- 
GitLab