From 66813e3968e4e5cf6d0749b7109ae3a9208e9fa9 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar Date: Fri, 14 Nov 2025 09:42:23 +0530 Subject: [PATCH 01/24] Added sample script which uses connectovercdp --- samples/cdp-tests/connectOverCDPScript.js | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 samples/cdp-tests/connectOverCDPScript.js diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js new file mode 100644 index 0000000..1b328c0 --- /dev/null +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -0,0 +1,36 @@ +import { chromium } from 'playwright'; + +(async () => { + try { + console.log('๐Ÿ”Œ Connecting to CDP server...'); + const browser = await chromium.connectOverCDP('ENTER YOUR CDP WEBSOCKET URL HERE', + {headers:{'User-Agent': 'Chrome-DevTools-Protocol/1.3'}}); + console.log('โœ… Connected successfully!'); + + const contexts = browser.contexts(); + let context; + if (contexts.length > 0) { + context = contexts[0]; + } else { + context = await browser.newContext(); + } + + const pages = context.pages(); + let page; + if (pages.length > 0) { + page = pages[0]; + } else { + page = await context.newPage(); + } + + console.log('๐ŸŒ Navigating to Google...'); + await page.goto('https://google.com'); + const title = await page.title(); + console.log('๐Ÿ“„ Page title:', title); + + // Don't close browser to keep it alive + await browser.close(); + } catch (error) { + console.error('โŒ Error:', error.message); + } +})(); \ No newline at end of file From e317281096af2813a445ce625393fc13e89b6e51 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar Date: Fri, 14 Nov 2025 09:52:40 +0530 Subject: [PATCH 02/24] . --- samples/cdp-tests/connectOverCDPScript.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 1b328c0..8b41e66 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -27,8 +27,7 @@ import { chromium } from 'playwright'; await page.goto('https://google.com'); const title = await page.title(); console.log('๐Ÿ“„ Page title:', title); - - // Don't close browser to keep it alive + await browser.close(); } catch (error) { console.error('โŒ Error:', error.message); From 232838df5889c53fa928d443233a692e7b6a6291 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:24:12 +0530 Subject: [PATCH 03/24] Add GetCdpUrl.py to retrieve WebSocket URL This script retrieves the WebSocket URL for a remote browser using Playwright's API, handling environment variables and making asynchronous HTTP requests. --- samples/cdp-tests/GetCdpUrl.py | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 samples/cdp-tests/GetCdpUrl.py diff --git a/samples/cdp-tests/GetCdpUrl.py b/samples/cdp-tests/GetCdpUrl.py new file mode 100644 index 0000000..800e759 --- /dev/null +++ b/samples/cdp-tests/GetCdpUrl.py @@ -0,0 +1,53 @@ +import aiohttp +import asyncio +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") +REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") +AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") + +if not all([WORKSPACE_ID, REGION, AUTH_TOKEN]): + raise RuntimeError( + "Missing required environment variables: " + "PLAYWRIGHT_SERVICE_WORKSPACE_ID, PLAYWRIGHT_SERVICE_REGION, PLAYWRIGHT_SERVICE_ACCESS_TOKEN" + ) + +async def get_remote_browser_websocket_url(): + api_url = ( + f"https://{REGION}.api.playwright.microsoft.com/" + f"playwrightworkspaces/{WORKSPACE_ID}/browsers" + "?os=linux&browser=chromium&playwrightVersion=cdp&shouldRedirect=false" + ) + headers = { + "Authorization": f"Bearer {AUTH_TOKEN}", + "Accept": "application/json", + "User-Agent": "PlaywrightService-CDP-Client/1.0" + } + async with aiohttp.ClientSession() as session: + async with session.get(api_url, headers=headers) as response: + data = await response.json() + ws_url = data.get('sessionUrl') or data.get('url') + if ws_url and ws_url.startswith('wss://'): + return ws_url + # If the first call returns an HTTP URL, make a second call + async with session.get(ws_url, headers=headers) as response2: + data2 = await response2.json() + final_ws_url = ( + data2.get('sessionUrl') or + data2.get('url') or + data2.get('webSocketUrl') or + data2.get('wsEndpoint') + ) + return final_ws_url + +# Usage example +async def main(): + ws_url = await get_remote_browser_websocket_url() + print("WebSocket URL:", ws_url) + +if __name__ == "__main__": + asyncio.run(main()) From 072daa6d69ba26fc52fdc3cdc96b1f80773fdd52 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:30:45 +0530 Subject: [PATCH 04/24] Create .env.example for Playwright configuration Add example environment configuration for Playwright service. --- samples/cdp-tests/.env.example | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 samples/cdp-tests/.env.example diff --git a/samples/cdp-tests/.env.example b/samples/cdp-tests/.env.example new file mode 100644 index 0000000..75b59cc --- /dev/null +++ b/samples/cdp-tests/.env.example @@ -0,0 +1,11 @@ +# Playwright Service Configuration +PLAYWRIGHT_SERVICE_WORKSPACE_ID=eecf0789-aadf-4a86-a4e3-6c61c3c59d67 +PLAYWRIGHT_SERVICE_REGION=eastasia +PLAYWRIGHT_SERVICE_ACCESS_TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6... + +# Optional: Custom run ID for test session tracking +# PLAYWRIGHT_SERVICE_RUN_ID=my-custom-run-id + +# Local testing configuration (when not using service) +HEADLESS=true +SLOW_MO=0 From bec9c07fc970a68b63dc59ee18bcf30bffe50c44 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:14:12 +0530 Subject: [PATCH 05/24] Fetch and connect to CDP WebSocket URL Added functionality to fetch CDP WebSocket URL from Playwright service and connect to the CDP server using it. --- samples/cdp-tests/connectOverCDPScript.js | 31 +++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 8b41e66..8976288 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -1,9 +1,36 @@ import { chromium } from 'playwright'; +const WORKSPACE_ID = process.env.PLAYWRIGHT_SERVICE_WORKSPACE_ID; +const REGION = process.env.PLAYWRIGHT_SERVICE_REGION; +const AUTH_TOKEN = process.env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN; + +async function getRemoteBrowserWebSocketUrl() { + const apiUrl = `https://${REGION}.api.playwright.microsoft.com/playwrightworkspaces/${WORKSPACE_ID}/browsers?os=linux&browser=chromium&playwrightVersion=cdp`; + const headers = { + "Authorization": `Bearer ${AUTH_TOKEN}`, + "Accept": "application/json", + "User-Agent": "PlaywrightService-CDP-Client/1.0" + }; + + const response = await fetch(apiUrl, { headers }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API request failed with status ${response.status}: ${errorText}`); + } + + const data = await response.json(); + return data.wsEndpoint; +} + (async () => { try { + console.log('๐Ÿ” Fetching CDP WebSocket URL...'); + const cdpUrl = await getRemoteBrowserWebSocketUrl(); + console.log('โœ… Got WebSocket URL:', cdpUrl); + console.log('๐Ÿ”Œ Connecting to CDP server...'); - const browser = await chromium.connectOverCDP('ENTER YOUR CDP WEBSOCKET URL HERE', + const browser = await chromium.connectOverCDP(cdpUrl, {headers:{'User-Agent': 'Chrome-DevTools-Protocol/1.3'}}); console.log('โœ… Connected successfully!'); @@ -32,4 +59,4 @@ import { chromium } from 'playwright'; } catch (error) { console.error('โŒ Error:', error.message); } -})(); \ No newline at end of file +})(); From 04f534d24d55ec5b8786a6934a4c2985541eff30 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:38:08 +0530 Subject: [PATCH 06/24] Delete samples/cdp-tests/GetCdpUrl.py --- samples/cdp-tests/GetCdpUrl.py | 53 ---------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 samples/cdp-tests/GetCdpUrl.py diff --git a/samples/cdp-tests/GetCdpUrl.py b/samples/cdp-tests/GetCdpUrl.py deleted file mode 100644 index 800e759..0000000 --- a/samples/cdp-tests/GetCdpUrl.py +++ /dev/null @@ -1,53 +0,0 @@ -import aiohttp -import asyncio -import os -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") -REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") -AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") - -if not all([WORKSPACE_ID, REGION, AUTH_TOKEN]): - raise RuntimeError( - "Missing required environment variables: " - "PLAYWRIGHT_SERVICE_WORKSPACE_ID, PLAYWRIGHT_SERVICE_REGION, PLAYWRIGHT_SERVICE_ACCESS_TOKEN" - ) - -async def get_remote_browser_websocket_url(): - api_url = ( - f"https://{REGION}.api.playwright.microsoft.com/" - f"playwrightworkspaces/{WORKSPACE_ID}/browsers" - "?os=linux&browser=chromium&playwrightVersion=cdp&shouldRedirect=false" - ) - headers = { - "Authorization": f"Bearer {AUTH_TOKEN}", - "Accept": "application/json", - "User-Agent": "PlaywrightService-CDP-Client/1.0" - } - async with aiohttp.ClientSession() as session: - async with session.get(api_url, headers=headers) as response: - data = await response.json() - ws_url = data.get('sessionUrl') or data.get('url') - if ws_url and ws_url.startswith('wss://'): - return ws_url - # If the first call returns an HTTP URL, make a second call - async with session.get(ws_url, headers=headers) as response2: - data2 = await response2.json() - final_ws_url = ( - data2.get('sessionUrl') or - data2.get('url') or - data2.get('webSocketUrl') or - data2.get('wsEndpoint') - ) - return final_ws_url - -# Usage example -async def main(): - ws_url = await get_remote_browser_websocket_url() - print("WebSocket URL:", ws_url) - -if __name__ == "__main__": - asyncio.run(main()) From 3aff18050584d452ca3fce0619655a0c321dc687 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:42:07 +0530 Subject: [PATCH 07/24] Add remote browser search functionality for Amazon --- samples/cdp-tests/Browser-Use-Remote.py | 181 ++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 samples/cdp-tests/Browser-Use-Remote.py diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py new file mode 100644 index 0000000..cd253e7 --- /dev/null +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -0,0 +1,181 @@ +import asyncio +import aiohttp +import os +from pydantic import BaseModel, Field +from browser_use import Agent +from browser_use.llm import ChatOllama +from browser_use.browser.session import BrowserSession +from browser_use.browser.profile import BrowserProfile + +# Remote browser connection details +WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") +REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") +AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") + + +async def get_remote_browser_websocket_url() -> str: + """Get the WebSocket URL for the remote Playwright browser""" + api_url = f"https://{REGION}.api.playwright.microsoft.com/playwrightworkspaces/{WORKSPACE_ID}/browsers?os=linux&browser=chromium&playwrightVersion=cdp" + + headers = { + "Authorization": f"Bearer {AUTH_TOKEN}", + "Accept": "application/json", + "User-Agent": "PlaywrightService-CDP-Client/1.0" + } + + async with aiohttp.ClientSession() as session: + async with session.get(api_url, headers=headers) as response: + if response.status != 200: + response_text = await response.text() + raise Exception(f"API call failed with status {response.status}: {response_text}") + + data = await response.json() + return data['wsEndpoint'] + + +async def create_remote_browser_session(ws_url: str): + """Create remote browser session using WebSocket URL""" + profile = BrowserProfile(cdp_url=ws_url) + return BrowserSession(browser_profile=profile) + + +class Product(BaseModel): + """A single Amazon product""" + name: str = Field(..., description='Product name') + price: str = Field(..., description='Displayed price text') + rating: str | None = Field(None, description='Rating stars') + reviews: str | None = Field(None, description='Number of reviews') + url: str = Field(..., description='Product page URL') + + +class ProductSearchResults(BaseModel): + """All found products""" + items: list[Product] = Field(default_factory=list, description='List of found products') + + +async def search_amazon_remote(keywords: str = 'wireless mouse'): + """Search Amazon using remote Playwright browser + local Ollama LLM""" + print(f"๐ŸŽฏ Searching Amazon for: {keywords}") + print("๐Ÿ”ง Using: Remote Playwright Browser + Local Ollama LLM") + + # Get remote browser WebSocket URL + print("๐Ÿ“ก Getting remote browser WebSocket URL...") + ws_url = await get_remote_browser_websocket_url() + + # Create remote browser session + print("๐Ÿ”Œ Creating remote browser session...") + browser_session = await create_remote_browser_session(ws_url) + + # Initialize Ollama LLM + print("๐Ÿค– Initializing Ollama LLM...") + llm = ChatOllama( + model="llama3.2:3b", + host="http://localhost:11434" + ) + + # Create agent + print("๐Ÿค– Creating Agent...") + agent = Agent( + task=f""" + Go to Amazon (https://www.amazon.com) and search for "{keywords}". + Collect the top 5 product results. + For each product, extract: + - Product name + - Price + - Rating (stars) + - Number of reviews + - Product page URL + Return structured output with all the details. + """, + llm=llm, + browser_session=browser_session, + output_model_schema=ProductSearchResults, + ) + + print("๐ŸŽฏ Starting Amazon search...") + result = await agent.run() + print("โœ… Search completed successfully!") + return result + + +async def search_amazon_local(keywords: str = 'wireless mouse'): + """Search Amazon using local browser as fallback""" + print(f"๐ŸŽฏ Searching Amazon for: {keywords}") + print("๐Ÿ”ง Using: Local Browser + Ollama LLM") + + # Initialize Ollama LLM + llm = ChatOllama( + model="llama3.2:3b", + host="http://localhost:11434" + ) + + # Create agent with local browser + agent = Agent( + task=f""" + Go to Amazon (https://www.amazon.com) and search for "{keywords}". + Collect the top 5 product results. + For each product, extract: + - Product name + - Price + - Rating (stars) + - Number of reviews + - Product page URL + Return structured output. + """, + llm=llm, + output_model_schema=ProductSearchResults, + ) + + print("๐ŸŽฏ Starting search...") + result = await agent.run() + print("โœ… Search completed successfully!") + return result + + +async def main(): + print("๐Ÿ›’ Amazon Product Search with Browser-Use + Ollama") + print("=" * 50) + + # Get user input + keywords = input('Enter product keywords (or press Enter for "wireless mouse"): ').strip() + if not keywords: + keywords = 'wireless mouse' + + # Ask user for browser choice + print("\nChoose browser option:") + print("1. Remote Browser (Microsoft Playwright Cloud)") + print("2. Local Browser") + + choice = input("Enter choice (1 or 2, default=2): ").strip() + + try: + if choice == '1': + result = await search_amazon_remote(keywords) + else: + result = await search_amazon_local(keywords) + + # Display results + if result and result.structured_output: + products = result.structured_output + print(f'\n{"=" * 70}') + print(f'Amazon Search Results for "{keywords}"') + print(f'{"=" * 70}\n') + + for i, item in enumerate(products.items, 1): + print(f'{i}. Name: {item.name}') + print(f' Price: {item.price}') + if item.rating: + print(f' Rating: {item.rating}') + if item.reviews: + print(f' Reviews: {item.reviews}') + print(f' URL: {item.url}') + print(f'{"-" * 70}') + else: + print("โŒ No products found or search failed") + + except Exception as e: + print(f"โŒ Error: {str(e)}") + + +if __name__ == '__main__': + asyncio.run(main()) From 0eac278df13f3f6aa4b15d3bcf302cd37ab923d6 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:15:55 +0530 Subject: [PATCH 08/24] Removed data from .env.example Clear sensitive information from the example environment file. --- samples/cdp-tests/.env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/cdp-tests/.env.example b/samples/cdp-tests/.env.example index 75b59cc..fd6387f 100644 --- a/samples/cdp-tests/.env.example +++ b/samples/cdp-tests/.env.example @@ -1,7 +1,7 @@ # Playwright Service Configuration -PLAYWRIGHT_SERVICE_WORKSPACE_ID=eecf0789-aadf-4a86-a4e3-6c61c3c59d67 -PLAYWRIGHT_SERVICE_REGION=eastasia -PLAYWRIGHT_SERVICE_ACCESS_TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6... +PLAYWRIGHT_SERVICE_WORKSPACE_ID= +PLAYWRIGHT_SERVICE_REGION= +PLAYWRIGHT_SERVICE_ACCESS_TOKEN= # Optional: Custom run ID for test session tracking # PLAYWRIGHT_SERVICE_RUN_ID=my-custom-run-id From 726a9024ebc3b13ffb6bed1f28eb83b9465ef7b4 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:53:40 +0530 Subject: [PATCH 09/24] Update to Azure OpenAI for product search Refactor to use Azure OpenAI instead of local Ollama LLM for Amazon product search. --- samples/cdp-tests/Browser-Use-Remote.py | 201 +++++++++++------------- 1 file changed, 92 insertions(+), 109 deletions(-) diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py index cd253e7..3abc244 100644 --- a/samples/cdp-tests/Browser-Use-Remote.py +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -1,115 +1,115 @@ +""" +Amazon Product Search with Browser-Use + Hosted LLM (Azure OpenAI) + +This script lets you search Amazon for products using a remote Playwright browser +(Microsoft Playwright Cloud) and a hosted LLM (Azure OpenAI). + +---------------------------------------- +๐Ÿ“Œ Prerequisites +---------------------------------------- +1๏ธโƒฃ Python environment with the following packages installed: + pip install aiohttp pydantic browser-use + +2๏ธโƒฃ Hosted LLM Setup (Azure OpenAI) + - Create an Azure OpenAI resource in the Azure Portal. + - Deploy a model (e.g., gpt-35-turbo). + - Set the following environment variables: + + export AZURE_OPENAI_API_KEY="your_api_key_here" + export AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/" + export AZURE_OPENAI_API_VERSION="2023-07-01-preview" + +3๏ธโƒฃ Playwright Remote Browser Setup + - Sign up for Microsoft Playwright Cloud. + - Obtain your workspace ID, region, and access token. + - Set the following environment variables: + + export PLAYWRIGHT_SERVICE_WORKSPACE_ID="your_workspace_id" + export PLAYWRIGHT_SERVICE_REGION="your_region" + export PLAYWRIGHT_SERVICE_ACCESS_TOKEN="your_access_token" + +---------------------------------------- +๐Ÿ“Œ How to Use +---------------------------------------- +1๏ธโƒฃ Run the script: + python Browser-Use-Remote.py + +2๏ธโƒฃ Enter product keywords when prompted. + (Default is "wireless mouse" if you press Enter.) + +3๏ธโƒฃ The script will connect to the remote browser and hosted LLM to perform + the search and print structured results in the terminal. +""" + import asyncio import aiohttp import os from pydantic import BaseModel, Field from browser_use import Agent -from browser_use.llm import ChatOllama +from browser_use.llm import AzureChatOpenAI from browser_use.browser.session import BrowserSession from browser_use.browser.profile import BrowserProfile -# Remote browser connection details -WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") -REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") -AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") +# --- Azure OpenAI Setup --- +def get_llm(): + """Initialize the hosted Azure OpenAI LLM.""" + return AzureChatOpenAI( + model_name="gpt-35-turbo", + openai_api_key=os.environ["AZURE_OPENAI_API_KEY"], + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + deployment_name="gpt-35-turbo", + max_tokens=3000, + api_version=os.environ["AZURE_OPENAI_API_VERSION"], + ) +# --- Remote Playwright Browser --- +async def create_remote_browser_session(): + """ + Create a remote Playwright browser session. + Fetches the WebSocket URL internally and returns a BrowserSession. + """ + WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") + REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") + AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") -async def get_remote_browser_websocket_url() -> str: - """Get the WebSocket URL for the remote Playwright browser""" api_url = f"https://{REGION}.api.playwright.microsoft.com/playwrightworkspaces/{WORKSPACE_ID}/browsers?os=linux&browser=chromium&playwrightVersion=cdp" - headers = { "Authorization": f"Bearer {AUTH_TOKEN}", "Accept": "application/json", "User-Agent": "PlaywrightService-CDP-Client/1.0" } - + async with aiohttp.ClientSession() as session: async with session.get(api_url, headers=headers) as response: if response.status != 200: - response_text = await response.text() - raise Exception(f"API call failed with status {response.status}: {response_text}") - + text = await response.text() + raise Exception(f"Failed to get remote browser URL: {response.status}, {text}") data = await response.json() - return data['wsEndpoint'] - + ws_url = data['wsEndpoint'] -async def create_remote_browser_session(ws_url: str): - """Create remote browser session using WebSocket URL""" profile = BrowserProfile(cdp_url=ws_url) return BrowserSession(browser_profile=profile) - +# --- Data Models --- class Product(BaseModel): - """A single Amazon product""" - name: str = Field(..., description='Product name') - price: str = Field(..., description='Displayed price text') - rating: str | None = Field(None, description='Rating stars') - reviews: str | None = Field(None, description='Number of reviews') - url: str = Field(..., description='Product page URL') - + name: str + price: str + rating: str | None = None + reviews: str | None = None + url: str class ProductSearchResults(BaseModel): - """All found products""" - items: list[Product] = Field(default_factory=list, description='List of found products') - + items: list[Product] = [] +# --- Amazon Search Function --- async def search_amazon_remote(keywords: str = 'wireless mouse'): - """Search Amazon using remote Playwright browser + local Ollama LLM""" + """Search Amazon using remote browser + hosted Azure OpenAI LLM""" print(f"๐ŸŽฏ Searching Amazon for: {keywords}") - print("๐Ÿ”ง Using: Remote Playwright Browser + Local Ollama LLM") - - # Get remote browser WebSocket URL - print("๐Ÿ“ก Getting remote browser WebSocket URL...") - ws_url = await get_remote_browser_websocket_url() - - # Create remote browser session - print("๐Ÿ”Œ Creating remote browser session...") - browser_session = await create_remote_browser_session(ws_url) - - # Initialize Ollama LLM - print("๐Ÿค– Initializing Ollama LLM...") - llm = ChatOllama( - model="llama3.2:3b", - host="http://localhost:11434" - ) - - # Create agent - print("๐Ÿค– Creating Agent...") - agent = Agent( - task=f""" - Go to Amazon (https://www.amazon.com) and search for "{keywords}". - Collect the top 5 product results. - For each product, extract: - - Product name - - Price - - Rating (stars) - - Number of reviews - - Product page URL - Return structured output with all the details. - """, - llm=llm, - browser_session=browser_session, - output_model_schema=ProductSearchResults, - ) - - print("๐ŸŽฏ Starting Amazon search...") - result = await agent.run() - print("โœ… Search completed successfully!") - return result + print("๐Ÿ”ง Using: Remote Playwright Browser + Azure OpenAI LLM") + browser_session = await create_remote_browser_session() + llm = get_llm() -async def search_amazon_local(keywords: str = 'wireless mouse'): - """Search Amazon using local browser as fallback""" - print(f"๐ŸŽฏ Searching Amazon for: {keywords}") - print("๐Ÿ”ง Using: Local Browser + Ollama LLM") - - # Initialize Ollama LLM - llm = ChatOllama( - model="llama3.2:3b", - host="http://localhost:11434" - ) - - # Create agent with local browser agent = Agent( task=f""" Go to Amazon (https://www.amazon.com) and search for "{keywords}". @@ -123,59 +123,42 @@ async def search_amazon_local(keywords: str = 'wireless mouse'): Return structured output. """, llm=llm, + browser_session=browser_session, output_model_schema=ProductSearchResults, ) - - print("๐ŸŽฏ Starting search...") + + print("๐ŸŽฏ Starting Amazon search...") result = await agent.run() print("โœ… Search completed successfully!") return result - +# --- Main --- async def main(): - print("๐Ÿ›’ Amazon Product Search with Browser-Use + Ollama") + print("๐Ÿ›’ Amazon Product Search with Browser-Use + Azure OpenAI") print("=" * 50) - # Get user input - keywords = input('Enter product keywords (or press Enter for "wireless mouse"): ').strip() - if not keywords: - keywords = 'wireless mouse' - - # Ask user for browser choice - print("\nChoose browser option:") - print("1. Remote Browser (Microsoft Playwright Cloud)") - print("2. Local Browser") - - choice = input("Enter choice (1 or 2, default=2): ").strip() - + keywords = input('Enter product keywords (default "wireless mouse"): ').strip() or 'wireless mouse' + try: - if choice == '1': - result = await search_amazon_remote(keywords) - else: - result = await search_amazon_local(keywords) + result = await search_amazon_remote(keywords) - # Display results if result and result.structured_output: products = result.structured_output print(f'\n{"=" * 70}') print(f'Amazon Search Results for "{keywords}"') print(f'{"=" * 70}\n') - for i, item in enumerate(products.items, 1): print(f'{i}. Name: {item.name}') print(f' Price: {item.price}') - if item.rating: - print(f' Rating: {item.rating}') - if item.reviews: - print(f' Reviews: {item.reviews}') + if item.rating: print(f' Rating: {item.rating}') + if item.reviews: print(f' Reviews: {item.reviews}') print(f' URL: {item.url}') print(f'{"-" * 70}') else: print("โŒ No products found or search failed") - - except Exception as e: - print(f"โŒ Error: {str(e)}") + except Exception as e: + print(f"โŒ Error: {e}") if __name__ == '__main__': asyncio.run(main()) From c2cb840375c829d476046e931068a64f3a566942 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:00:10 +0530 Subject: [PATCH 10/24] Change console.log to console.debug and update URL --- samples/cdp-tests/connectOverCDPScript.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 8976288..5848949 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -27,7 +27,7 @@ async function getRemoteBrowserWebSocketUrl() { try { console.log('๐Ÿ” Fetching CDP WebSocket URL...'); const cdpUrl = await getRemoteBrowserWebSocketUrl(); - console.log('โœ… Got WebSocket URL:', cdpUrl); + console.debug('โœ… Got WebSocket URL:', cdpUrl); console.log('๐Ÿ”Œ Connecting to CDP server...'); const browser = await chromium.connectOverCDP(cdpUrl, @@ -51,7 +51,7 @@ async function getRemoteBrowserWebSocketUrl() { } console.log('๐ŸŒ Navigating to Google...'); - await page.goto('https://google.com'); + await page.goto('https://playwright.dev'); const title = await page.title(); console.log('๐Ÿ“„ Page title:', title); From 85ebc94b71fbe6d8258d484529c8e0b00bd84ee6 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:52:55 +0530 Subject: [PATCH 11/24] Refactor connection script for Playwright service --- samples/cdp-tests/connectOverCDPScript.js | 41 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 5848949..4af44d5 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -1,13 +1,38 @@ import { chromium } from 'playwright'; -const WORKSPACE_ID = process.env.PLAYWRIGHT_SERVICE_WORKSPACE_ID; -const REGION = process.env.PLAYWRIGHT_SERVICE_REGION; -const AUTH_TOKEN = process.env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN; +const SERVICE_URL = process.env.PLAYWRIGHT_SERVICE_URL; +const ACCESS_TOKEN = process.env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN; + +// Parse region and workspaceId from PLAYWRIGHT_SERVICE_URL +// Format: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers +function parseServiceUrl(url: string) { + if (!url) { + throw new Error('PLAYWRIGHT_SERVICE_URL environment variable is not set'); + } + + const urlPattern = /wss:\/\/(\w+)\.api\.playwright\.microsoft\.com\/playwrightworkspaces\/([^\/]+)\/browsers/; + const match = url.match(urlPattern); + + if (!match) { + throw new Error('Invalid PLAYWRIGHT_SERVICE_URL format. Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers'); + } + + return { + region: match[1], + workspaceId: match[2] + }; +} async function getRemoteBrowserWebSocketUrl() { - const apiUrl = `https://${REGION}.api.playwright.microsoft.com/playwrightworkspaces/${WORKSPACE_ID}/browsers?os=linux&browser=chromium&playwrightVersion=cdp`; + if (!ACCESS_TOKEN) { + throw new Error('PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set'); + } + + const { region, workspaceId } = parseServiceUrl(SERVICE_URL!); + + const apiUrl = `https://${region}.api.playwright.microsoft.com/playwrightworkspaces/${workspaceId}/browsers?os=linux&browser=chromium&playwrightVersion=cdp`; const headers = { - "Authorization": `Bearer ${AUTH_TOKEN}`, + "Authorization": `Bearer ${ACCESS_TOKEN}`, "Accept": "application/json", "User-Agent": "PlaywrightService-CDP-Client/1.0" }; @@ -20,7 +45,7 @@ async function getRemoteBrowserWebSocketUrl() { } const data = await response.json(); - return data.wsEndpoint; + return data.endpoint; // โœ… Fixed: use 'endpoint' not 'wsEndpoint' } (async () => { @@ -50,13 +75,13 @@ async function getRemoteBrowserWebSocketUrl() { page = await context.newPage(); } - console.log('๐ŸŒ Navigating to Google...'); + console.log('๐ŸŒ Navigating to playwright.dev...'); await page.goto('https://playwright.dev'); const title = await page.title(); console.log('๐Ÿ“„ Page title:', title); await browser.close(); - } catch (error) { + } catch (error: any) { console.error('โŒ Error:', error.message); } })(); From c703f1228b5784ed9b39ee2493df4a303df9ddfc Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:56:59 +0530 Subject: [PATCH 12/24] Refactor Playwright service URL handling Updated environment variable usage and added parsing for service URL. --- samples/cdp-tests/Browser-Use-Remote.py | 38 +++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py index 3abc244..0989a00 100644 --- a/samples/cdp-tests/Browser-Use-Remote.py +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -21,11 +21,10 @@ 3๏ธโƒฃ Playwright Remote Browser Setup - Sign up for Microsoft Playwright Cloud. - - Obtain your workspace ID, region, and access token. + - Obtain your service URL and access token. - Set the following environment variables: - export PLAYWRIGHT_SERVICE_WORKSPACE_ID="your_workspace_id" - export PLAYWRIGHT_SERVICE_REGION="your_region" + export PLAYWRIGHT_SERVICE_URL="wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers" export PLAYWRIGHT_SERVICE_ACCESS_TOKEN="your_access_token" ---------------------------------------- @@ -44,6 +43,7 @@ import asyncio import aiohttp import os +import re from pydantic import BaseModel, Field from browser_use import Agent from browser_use.llm import AzureChatOpenAI @@ -63,18 +63,38 @@ def get_llm(): ) # --- Remote Playwright Browser --- +def parse_service_url(service_url: str) -> tuple[str, str]: + """ + Parse the Playwright Service URL to extract region and workspace ID. + Expected format: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers + """ + pattern = r'wss://(\w+)\.api\.playwright\.microsoft\.com/playwrightworkspaces/([^/]+)/browsers' + match = re.match(pattern, service_url) + if not match: + raise ValueError( + f"Invalid PLAYWRIGHT_SERVICE_URL format. Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers" + ) + return match.group(1), match.group(2) # region, workspaceId + async def create_remote_browser_session(): """ Create a remote Playwright browser session. Fetches the WebSocket URL internally and returns a BrowserSession. """ - WORKSPACE_ID = os.getenv("PLAYWRIGHT_SERVICE_WORKSPACE_ID") - REGION = os.getenv("PLAYWRIGHT_SERVICE_REGION") - AUTH_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") + SERVICE_URL = os.getenv("PLAYWRIGHT_SERVICE_URL") + ACCESS_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") + + if not SERVICE_URL: + raise ValueError("PLAYWRIGHT_SERVICE_URL environment variable is not set") + if not ACCESS_TOKEN: + raise ValueError("PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set") + + # Parse region and workspace ID from the service URL + region, workspace_id = parse_service_url(SERVICE_URL) - api_url = f"https://{REGION}.api.playwright.microsoft.com/playwrightworkspaces/{WORKSPACE_ID}/browsers?os=linux&browser=chromium&playwrightVersion=cdp" + api_url = f"https://{region}.api.playwright.microsoft.com/playwrightworkspaces/{workspace_id}/browsers?os=linux&browser=chromium&playwrightVersion=cdp" headers = { - "Authorization": f"Bearer {AUTH_TOKEN}", + "Authorization": f"Bearer {ACCESS_TOKEN}", "Accept": "application/json", "User-Agent": "PlaywrightService-CDP-Client/1.0" } @@ -85,7 +105,7 @@ async def create_remote_browser_session(): text = await response.text() raise Exception(f"Failed to get remote browser URL: {response.status}, {text}") data = await response.json() - ws_url = data['wsEndpoint'] + ws_url = data['endpoint'] # API returns 'endpoint', not 'wsEndpoint' profile = BrowserProfile(cdp_url=ws_url) return BrowserSession(browser_profile=profile) From e628926d0fe979b452c7117b952cfac22e4f7538 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:35:19 +0530 Subject: [PATCH 13/24] Refactor Browser-Use-Remote.py for clarity and dotenv Updated prerequisites and usage instructions. Refactored remote browser session creation and added dotenv support. --- samples/cdp-tests/Browser-Use-Remote.py | 77 ++++++++++--------------- 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py index 0989a00..3c7a8b2 100644 --- a/samples/cdp-tests/Browser-Use-Remote.py +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -8,7 +8,7 @@ ๐Ÿ“Œ Prerequisites ---------------------------------------- 1๏ธโƒฃ Python environment with the following packages installed: - pip install aiohttp pydantic browser-use + pip install aiohttp pydantic browser-use python-dotenv 2๏ธโƒฃ Hosted LLM Setup (Azure OpenAI) - Create an Azure OpenAI resource in the Azure Portal. @@ -31,7 +31,7 @@ ๐Ÿ“Œ How to Use ---------------------------------------- 1๏ธโƒฃ Run the script: - python Browser-Use-Remote.py + python browser_use_remote.py 2๏ธโƒฃ Enter product keywords when prompted. (Default is "wireless mouse" if you press Enter.) @@ -41,15 +41,21 @@ """ import asyncio -import aiohttp import os -import re -from pydantic import BaseModel, Field +from pydantic import BaseModel +from dotenv import load_dotenv from browser_use import Agent from browser_use.llm import AzureChatOpenAI from browser_use.browser.session import BrowserSession from browser_use.browser.profile import BrowserProfile +# Load environment variables from .env file +load_dotenv() + +# Import the shared module +from playwright_service_client import PlaywrightServiceClient + + # --- Azure OpenAI Setup --- def get_llm(): """Initialize the hosted Azure OpenAI LLM.""" @@ -62,54 +68,25 @@ def get_llm(): api_version=os.environ["AZURE_OPENAI_API_VERSION"], ) + # --- Remote Playwright Browser --- -def parse_service_url(service_url: str) -> tuple[str, str]: +async def create_remote_browser_session() -> BrowserSession: """ - Parse the Playwright Service URL to extract region and workspace ID. - Expected format: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers + Create a remote Playwright browser session using the shared client. + Returns a BrowserSession configured for browser-use. """ - pattern = r'wss://(\w+)\.api\.playwright\.microsoft\.com/playwrightworkspaces/([^/]+)/browsers' - match = re.match(pattern, service_url) - if not match: - raise ValueError( - f"Invalid PLAYWRIGHT_SERVICE_URL format. Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers" - ) - return match.group(1), match.group(2) # region, workspaceId - -async def create_remote_browser_session(): - """ - Create a remote Playwright browser session. - Fetches the WebSocket URL internally and returns a BrowserSession. - """ - SERVICE_URL = os.getenv("PLAYWRIGHT_SERVICE_URL") - ACCESS_TOKEN = os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") - - if not SERVICE_URL: - raise ValueError("PLAYWRIGHT_SERVICE_URL environment variable is not set") - if not ACCESS_TOKEN: - raise ValueError("PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set") - - # Parse region and workspace ID from the service URL - region, workspace_id = parse_service_url(SERVICE_URL) - - api_url = f"https://{region}.api.playwright.microsoft.com/playwrightworkspaces/{workspace_id}/browsers?os=linux&browser=chromium&playwrightVersion=cdp" - headers = { - "Authorization": f"Bearer {ACCESS_TOKEN}", - "Accept": "application/json", - "User-Agent": "PlaywrightService-CDP-Client/1.0" - } - - async with aiohttp.ClientSession() as session: - async with session.get(api_url, headers=headers) as response: - if response.status != 200: - text = await response.text() - raise Exception(f"Failed to get remote browser URL: {response.status}, {text}") - data = await response.json() - ws_url = data['endpoint'] # API returns 'endpoint', not 'wsEndpoint' - - profile = BrowserProfile(cdp_url=ws_url) + # Use the shared client to get the CDP endpoint + client = PlaywrightServiceClient.from_env() + cdp_url = await client.get_cdp_endpoint() + + print(f"๐Ÿ”— Connected to Playwright Service") + print(f" Region: {client.region}") + print(f" Workspace: {client.workspace_id}") + + profile = BrowserProfile(cdp_url=cdp_url) return BrowserSession(browser_profile=profile) + # --- Data Models --- class Product(BaseModel): name: str @@ -118,9 +95,11 @@ class Product(BaseModel): reviews: str | None = None url: str + class ProductSearchResults(BaseModel): items: list[Product] = [] + # --- Amazon Search Function --- async def search_amazon_remote(keywords: str = 'wireless mouse'): """Search Amazon using remote browser + hosted Azure OpenAI LLM""" @@ -152,6 +131,7 @@ async def search_amazon_remote(keywords: str = 'wireless mouse'): print("โœ… Search completed successfully!") return result + # --- Main --- async def main(): print("๐Ÿ›’ Amazon Product Search with Browser-Use + Azure OpenAI") @@ -180,5 +160,6 @@ async def main(): except Exception as e: print(f"โŒ Error: {e}") + if __name__ == '__main__': asyncio.run(main()) From 86e0f351ca378c720636dd1648f6c0786aa4a1f8 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:37:06 +0530 Subject: [PATCH 14/24] Refactor connectOverCDPScript.js to Python Updated the script to connect to a remote browser via CDP using Python. Added prerequisites and usage instructions. --- samples/cdp-tests/connectOverCDPScript.js | 157 +++++++++++----------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 4af44d5..26ee925 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -1,87 +1,90 @@ -import { chromium } from 'playwright'; +""" +Connect Over CDP - Microsoft Playwright Service -const SERVICE_URL = process.env.PLAYWRIGHT_SERVICE_URL; -const ACCESS_TOKEN = process.env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN; +Simple example showing how to connect to a remote browser via CDP. +This demonstrates a NON-TESTING scenario for manual browser automation. -// Parse region and workspaceId from PLAYWRIGHT_SERVICE_URL -// Format: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers -function parseServiceUrl(url: string) { - if (!url) { - throw new Error('PLAYWRIGHT_SERVICE_URL environment variable is not set'); - } - - const urlPattern = /wss:\/\/(\w+)\.api\.playwright\.microsoft\.com\/playwrightworkspaces\/([^\/]+)\/browsers/; - const match = url.match(urlPattern); - - if (!match) { - throw new Error('Invalid PLAYWRIGHT_SERVICE_URL format. Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers'); - } - - return { - region: match[1], - workspaceId: match[2] - }; -} +---------------------------------------- +๐Ÿ“Œ Prerequisites +---------------------------------------- +1๏ธโƒฃ Python environment with the following packages installed: + pip install playwright aiohttp python-dotenv -async function getRemoteBrowserWebSocketUrl() { - if (!ACCESS_TOKEN) { - throw new Error('PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set'); - } +2๏ธโƒฃ Playwright Remote Browser Setup + - Sign up for Microsoft Playwright Service. + - Obtain your service URL and access token. + - Copy .env.example to .env and fill in your values: - const { region, workspaceId } = parseServiceUrl(SERVICE_URL!); - - const apiUrl = `https://${region}.api.playwright.microsoft.com/playwrightworkspaces/${workspaceId}/browsers?os=linux&browser=chromium&playwrightVersion=cdp`; - const headers = { - "Authorization": `Bearer ${ACCESS_TOKEN}`, - "Accept": "application/json", - "User-Agent": "PlaywrightService-CDP-Client/1.0" - }; + PLAYWRIGHT_SERVICE_URL=wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers + PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token - const response = await fetch(apiUrl, { headers }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API request failed with status ${response.status}: ${errorText}`); - } +---------------------------------------- +๐Ÿ“Œ How to Use +---------------------------------------- +1๏ธโƒฃ Run the script: + python connect_cdp.py - const data = await response.json(); - return data.endpoint; // โœ… Fixed: use 'endpoint' not 'wsEndpoint' -} +2๏ธโƒฃ The script will: + - Connect to the remote browser + - Navigate to example.com + - Take a screenshot + - Extract page content + - Click a link +""" -(async () => { - try { - console.log('๐Ÿ” Fetching CDP WebSocket URL...'); - const cdpUrl = await getRemoteBrowserWebSocketUrl(); - console.debug('โœ… Got WebSocket URL:', cdpUrl); - - console.log('๐Ÿ”Œ Connecting to CDP server...'); - const browser = await chromium.connectOverCDP(cdpUrl, - {headers:{'User-Agent': 'Chrome-DevTools-Protocol/1.3'}}); - console.log('โœ… Connected successfully!'); +import asyncio +from playwright.async_api import async_playwright +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +from playwright_service_client import get_cdp_endpoint + + +async def main(): + """Connect to a remote browser via CDP and perform basic operations.""" - const contexts = browser.contexts(); - let context; - if (contexts.length > 0) { - context = contexts[0]; - } else { - context = await browser.newContext(); - } + print("๐Ÿ”— Connecting to Microsoft Playwright Service...") - const pages = context.pages(); - let page; - if (pages.length > 0) { - page = pages[0]; - } else { - page = await context.newPage(); - } + # Step 1: Get CDP endpoint from the service + cdp_url = await get_cdp_endpoint() + print(f"โœ… Got CDP endpoint") - console.log('๐ŸŒ Navigating to playwright.dev...'); - await page.goto('https://playwright.dev'); - const title = await page.title(); - console.log('๐Ÿ“„ Page title:', title); - - await browser.close(); - } catch (error: any) { - console.error('โŒ Error:', error.message); - } -})(); + # Step 2: Connect to remote browser using Playwright + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(cdp_url) + print(f"โœ… Connected to remote browser") + + # Step 3: Use the browser + context = await browser.new_context() + page = await context.new_page() + + # Example: Navigate and take screenshot + print("๐Ÿ“„ Navigating to example.com...") + await page.goto("https://example.com") + + title = await page.title() + print(f"๐Ÿ“Œ Page title: {title}") + + # Take a screenshot + await page.screenshot(path="screenshot.png") + print("๐Ÿ“ธ Screenshot saved to screenshot.png") + + # Example: Extract content + heading = await page.locator("h1").text_content() + print(f"๐Ÿ“ Page heading: {heading}") + + # Example: Click a link + await page.click("a") + await page.wait_for_load_state("networkidle") + print(f"๐Ÿ”— Navigated to: {page.url}") + + # Cleanup + await context.close() + await browser.close() + print("โœ… Done!") + + +if __name__ == "__main__": + asyncio.run(main()) From 03711572fa84f83b93293119a97c7e254b324f46 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:37:58 +0530 Subject: [PATCH 15/24] Revise .env.example for Playwright and Azure OpenAI Updated environment variable placeholders for Playwright and Azure OpenAI. --- samples/cdp-tests/.env.example | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/samples/cdp-tests/.env.example b/samples/cdp-tests/.env.example index fd6387f..7b26d57 100644 --- a/samples/cdp-tests/.env.example +++ b/samples/cdp-tests/.env.example @@ -1,11 +1,11 @@ -# Playwright Service Configuration -PLAYWRIGHT_SERVICE_WORKSPACE_ID= -PLAYWRIGHT_SERVICE_REGION= -PLAYWRIGHT_SERVICE_ACCESS_TOKEN= +# Microsoft Playwright Service - Environment Variables +# Copy this file to .env and fill in your values -# Optional: Custom run ID for test session tracking -# PLAYWRIGHT_SERVICE_RUN_ID=my-custom-run-id +# Playwright Service (Required for all samples) +PLAYWRIGHT_SERVICE_URL= +PLAYWRIGHT_SERVICE_ACCESS_TOKEN= -# Local testing configuration (when not using service) -HEADLESS=true -SLOW_MO=0 +# Azure OpenAI (Required for browser_use_remote.py only) +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_API_VERSION= From 909d57d40d2372bfbbf94634c54d9db71be1947f Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:00:18 +0530 Subject: [PATCH 16/24] Refactor Browser-Use-Remote.py for clarity and updates Updated script usage instructions and refactored remote browser session creation. --- samples/cdp-tests/Browser-Use-Remote.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py index 3c7a8b2..d743179 100644 --- a/samples/cdp-tests/Browser-Use-Remote.py +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -31,7 +31,7 @@ ๐Ÿ“Œ How to Use ---------------------------------------- 1๏ธโƒฃ Run the script: - python browser_use_remote.py + python example_ai_agent.py 2๏ธโƒฃ Enter product keywords when prompted. (Default is "wireless mouse" if you press Enter.) @@ -53,7 +53,7 @@ load_dotenv() # Import the shared module -from playwright_service_client import PlaywrightServiceClient +from playwright_service_client import get_cdp_endpoint # --- Azure OpenAI Setup --- @@ -72,16 +72,11 @@ def get_llm(): # --- Remote Playwright Browser --- async def create_remote_browser_session() -> BrowserSession: """ - Create a remote Playwright browser session using the shared client. + Create a remote Playwright browser session. Returns a BrowserSession configured for browser-use. """ - # Use the shared client to get the CDP endpoint - client = PlaywrightServiceClient.from_env() - cdp_url = await client.get_cdp_endpoint() - + cdp_url = await get_cdp_endpoint() print(f"๐Ÿ”— Connected to Playwright Service") - print(f" Region: {client.region}") - print(f" Workspace: {client.workspace_id}") profile = BrowserProfile(cdp_url=cdp_url) return BrowserSession(browser_profile=profile) From 36a05efd3385f98ad1a2f385b97a6ec458e68d04 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:05:03 +0530 Subject: [PATCH 17/24] Add Playwright Service Python client implementation This file provides a Python client for Microsoft Playwright Service, including methods to retrieve a CDP endpoint URL for connecting to remote browsers. It includes error handling and environment variable support. --- .../cdp-tests/playwright_service_client.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 samples/cdp-tests/playwright_service_client.py diff --git a/samples/cdp-tests/playwright_service_client.py b/samples/cdp-tests/playwright_service_client.py new file mode 100644 index 0000000..ca4815d --- /dev/null +++ b/samples/cdp-tests/playwright_service_client.py @@ -0,0 +1,119 @@ +""" +Microsoft Playwright Service - Python Client + +Get a CDP endpoint URL to connect to remote browsers. + +---------------------------------------- +๐Ÿ“Œ Prerequisites +---------------------------------------- +pip install aiohttp python-dotenv + +---------------------------------------- +๐Ÿ“Œ Environment Variables +---------------------------------------- +PLAYWRIGHT_SERVICE_URL=wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers +PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token + +---------------------------------------- +๐Ÿ“Œ How to Use +---------------------------------------- + from playwright_service_client import get_cdp_endpoint + + cdp_url = await get_cdp_endpoint() + browser = await playwright.chromium.connect_over_cdp(cdp_url) +""" + +import re +import os +import aiohttp +from dotenv import load_dotenv + +load_dotenv() + + +class PlaywrightServiceError(Exception): + """Exception for Playwright Service errors.""" + pass + + +# URL pattern: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers +_URL_PATTERN = re.compile( + r'wss://(\w+)\.api\.playwright\.microsoft\.com/playwrightworkspaces/([^/]+)/browsers' +) + + +def _parse_url(url: str) -> tuple[str, str]: + """Extract region and workspace ID from service URL.""" + match = _URL_PATTERN.match(url) + if not match: + raise PlaywrightServiceError( + f"Invalid PLAYWRIGHT_SERVICE_URL format: {url}\n" + f"Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers" + ) + return match.group(1), match.group(2) + + +async def get_cdp_endpoint( + service_url: str | None = None, + access_token: str | None = None, + os_name: str = "Linux", + api_version: str = "2025-09-01" +) -> str: + """ + Get a CDP endpoint URL from Microsoft Playwright Service. + + Args: + service_url: Service URL (defaults to PLAYWRIGHT_SERVICE_URL env var) + access_token: Access token (defaults to PLAYWRIGHT_SERVICE_ACCESS_TOKEN env var) + os_name: Browser OS - "Linux" or "Windows" (default: Linux) + api_version: API version (default: 2025-09-01) + + Returns: + WebSocket URL for CDP connection + + Example: + cdp_url = await get_cdp_endpoint() + browser = await playwright.chromium.connect_over_cdp(cdp_url) + """ + # Get credentials from env vars if not provided + service_url = service_url or os.getenv("PLAYWRIGHT_SERVICE_URL") + access_token = access_token or os.getenv("PLAYWRIGHT_SERVICE_ACCESS_TOKEN") + + if not service_url: + raise PlaywrightServiceError( + "PLAYWRIGHT_SERVICE_URL environment variable is not set.\n" + "Expected: wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers" + ) + if not access_token: + raise PlaywrightServiceError( + "PLAYWRIGHT_SERVICE_ACCESS_TOKEN environment variable is not set." + ) + + # Parse URL to get region and workspace ID + region, workspace_id = _parse_url(service_url) + + # Build API URL + api_url = ( + f"https://{region}.api.playwright.microsoft.com" + f"/playwrightworkspaces/{workspace_id}/browsers" + f"?api-version={api_version}&os={os_name}" + ) + + # Make request + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/json", + } + + async with aiohttp.ClientSession() as session: + async with session.get(api_url, headers=headers) as response: + if response.status == 401: + raise PlaywrightServiceError("Authentication failed. Check your access token.") + if response.status == 403: + raise PlaywrightServiceError("Access forbidden. Check your permissions.") + if response.status != 200: + text = await response.text() + raise PlaywrightServiceError(f"Failed to get browser endpoint: HTTP {response.status}\n{text}") + + data = await response.json() + return data["endpoint"] From 3236193f7647581da7cb492d813739dab112ca45 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:06:21 +0530 Subject: [PATCH 18/24] Add Playwright testing example with remote browser support This script provides a testing framework using Microsoft Playwright Service to run tests on remote browsers via the Chrome DevTools Protocol. It includes context managers for easy browser and page access, example tests, and a test runner. --- samples/cdp-tests/test_runner.py | 177 +++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 samples/cdp-tests/test_runner.py diff --git a/samples/cdp-tests/test_runner.py b/samples/cdp-tests/test_runner.py new file mode 100644 index 0000000..1138aa2 --- /dev/null +++ b/samples/cdp-tests/test_runner.py @@ -0,0 +1,177 @@ +""" +Playwright Testing Example - Microsoft Playwright Service + +Run Playwright tests on remote browsers via CDP (Chrome DevTools Protocol). + +---------------------------------------- +๐Ÿ“Œ Prerequisites +---------------------------------------- +1๏ธโƒฃ Python environment with the following packages installed: + pip install playwright aiohttp pytest pytest-asyncio python-dotenv + +2๏ธโƒฃ Playwright Remote Browser Setup + - Sign up for Microsoft Playwright Service. + - Obtain your service URL and access token. + - Copy .env.example to .env and fill in your values: + + PLAYWRIGHT_SERVICE_URL=wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers + PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token + +---------------------------------------- +๐Ÿ“Œ How to Use +---------------------------------------- +1๏ธโƒฃ Run as a standalone script: + python test_runner.py + +2๏ธโƒฃ Run with pytest: + pytest test_runner.py -v + +3๏ธโƒฃ Import and use in your own tests: + from test_runner import remote_page + + async with remote_page() as page: + await page.goto("https://example.com") + assert await page.title() == "Example Domain" +""" + +import asyncio +from contextlib import asynccontextmanager +from typing import AsyncGenerator +from playwright.async_api import async_playwright, Browser, Page +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# Import the shared module +from playwright_service_client import get_cdp_endpoint + + +# ============================================================================ +# Context Managers for Easy Test Setup +# ============================================================================ + +@asynccontextmanager +async def remote_browser() -> AsyncGenerator[Browser, None]: + """ + Context manager for quick access to a remote browser. + + Example: + async with remote_browser() as browser: + page = await browser.new_page() + await page.goto("https://example.com") + """ + cdp_url = await get_cdp_endpoint() + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(cdp_url) + try: + yield browser + finally: + await browser.close() + + +@asynccontextmanager +async def remote_page() -> AsyncGenerator[Page, None]: + """ + Context manager for quick access to a remote page. + + Example: + async with remote_page() as page: + await page.goto("https://example.com") + assert await page.title() == "Example Domain" + """ + async with remote_browser() as browser: + context = await browser.new_context() + page = await context.new_page() + try: + yield page + finally: + await context.close() + + +# ============================================================================ +# Example Tests +# ============================================================================ + +async def test_example_domain(): + """Test that example.com loads correctly.""" + async with remote_page() as page: + await page.goto("https://example.com") + + title = await page.title() + assert title == "Example Domain", f"Expected 'Example Domain', got '{title}'" + + heading = await page.locator("h1").text_content() + assert heading == "Example Domain" + + print("โœ… test_example_domain passed!") + + +async def test_navigation(): + """Test basic navigation functionality.""" + async with remote_page() as page: + await page.goto("https://example.com") + initial_url = page.url + + # Click the "More information..." link + await page.click("a") + await page.wait_for_load_state("networkidle") + + assert page.url != initial_url, "URL should have changed after clicking link" + print("โœ… test_navigation passed!") + + +async def test_screenshot(): + """Test taking a screenshot.""" + async with remote_page() as page: + await page.goto("https://example.com") + + screenshot = await page.screenshot() + assert len(screenshot) > 0, "Screenshot should not be empty" + + print(f"โœ… test_screenshot passed! ({len(screenshot)} bytes)") + + +# ============================================================================ +# Test Runner +# ============================================================================ + +async def run_all_tests(): + """Run all example tests.""" + print("=" * 50) + print("๐Ÿงช Running Playwright Service Tests") + print("=" * 50) + + tests = [ + ("Example Domain", test_example_domain), + ("Navigation", test_navigation), + ("Screenshot", test_screenshot), + ] + + passed = failed = 0 + + for name, test_func in tests: + print(f"\n๐Ÿ“‹ {name}") + print("-" * 30) + try: + await test_func() + passed += 1 + except Exception as e: + print(f"โŒ Failed: {e}") + failed += 1 + + print(f"\n{'=' * 50}") + print(f"๐Ÿ“Š Results: {passed} passed, {failed} failed") + print("=" * 50) + + return failed == 0 + + +# ============================================================================ +# Main +# ============================================================================ + +if __name__ == "__main__": + print("๐Ÿงช Playwright Testing - Microsoft Playwright Service\n") + success = asyncio.run(run_all_tests()) + exit(0 if success else 1) From 9a00d32de7cf8f6ad8070501bae91a9fb22d3769 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:09:52 +0530 Subject: [PATCH 19/24] Update script name in usage instructions --- samples/cdp-tests/Browser-Use-Remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/cdp-tests/Browser-Use-Remote.py b/samples/cdp-tests/Browser-Use-Remote.py index d743179..4d7549d 100644 --- a/samples/cdp-tests/Browser-Use-Remote.py +++ b/samples/cdp-tests/Browser-Use-Remote.py @@ -31,7 +31,7 @@ ๐Ÿ“Œ How to Use ---------------------------------------- 1๏ธโƒฃ Run the script: - python example_ai_agent.py + python Browser-Use-Remote.py 2๏ธโƒฃ Enter product keywords when prompted. (Default is "wireless mouse" if you press Enter.) From 3fc4bfef2a44aeda6b564542cd13ab913d7dfc2d Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:10:33 +0530 Subject: [PATCH 20/24] Fix script name in usage instructions --- samples/cdp-tests/connectOverCDPScript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 26ee925..8bfe2a1 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -22,7 +22,7 @@ This demonstrates a NON-TESTING scenario for manual browser automation. ๐Ÿ“Œ How to Use ---------------------------------------- 1๏ธโƒฃ Run the script: - python connect_cdp.py + python connectOverCDPScript.py 2๏ธโƒฃ The script will: - Connect to the remote browser From 31e1b879ebf198c9011c1ce72e669438da300eef Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:11:44 +0530 Subject: [PATCH 21/24] Add README for Python CDP samples Added a README file with usage instructions and examples for Python CDP samples using Microsoft Playwright Service. --- samples/cdp-tests/README.md | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 samples/cdp-tests/README.md diff --git a/samples/cdp-tests/README.md b/samples/cdp-tests/README.md new file mode 100644 index 0000000..a3abd97 --- /dev/null +++ b/samples/cdp-tests/README.md @@ -0,0 +1,81 @@ +# Microsoft Playwright Service - Python CDP Samples + +Python samples for connecting to Microsoft Playwright Service via CDP. + +## ๐Ÿ“ Files + +| File | Use Case | Description | +|------|----------|-------------| +| `playwright_service_client.py` | Core Module | Shared client for all samples | +| `test_runner.py` | **Testing** | Test runner with helpers | +| `connectOverCDPScript.py` | **Manual** | Simple connect_over_cdp example | +| `Browser-Use-Remote.py` | **AI Agent** | Browser-Use + Azure OpenAI | + +## ๐Ÿš€ Quick Start + +```bash +# Install dependencies +pip install -r requirements.txt + +# Copy .env.example to .env and fill in your values +cp .env.example .env + +# Run samples +python connectOverCDPScript.py # Basic CDP connection +python test_runner.py # Run example tests +pytest test_runner.py -v # With pytest +python Browser-Use-Remote.py # AI agent (requires Azure OpenAI) +``` + +## ๐Ÿ“– Usage Examples + +### Basic CDP Connection +```python +from playwright.async_api import async_playwright +from playwright_service_client import get_cdp_endpoint + +cdp_url = await get_cdp_endpoint() +async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(cdp_url) + page = await browser.new_page() + await page.goto("https://example.com") +``` + +### Test Automation +```python +from test_runner import remote_page + +async with remote_page() as page: + await page.goto("https://example.com") + assert await page.title() == "Example Domain" +``` + +### AI Agent +```python +from playwright_service_client import get_cdp_endpoint +from browser_use.browser.profile import BrowserProfile + +cdp_url = await get_cdp_endpoint() +profile = BrowserProfile(cdp_url=cdp_url) +``` + +## ๐Ÿ”ง Environment Variables + +Copy `.env.example` to `.env` and fill in your values: + +```bash +PLAYWRIGHT_SERVICE_URL=wss://.api.playwright.microsoft.com/playwrightworkspaces//browsers +PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token + +# For AI agent example only +AZURE_OPENAI_API_KEY=your_api_key +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ +AZURE_OPENAI_API_VERSION=2023-07-01-preview +``` + +## ๐Ÿ“š Resources + +- [Microsoft Playwright Service](https://learn.microsoft.com/azure/playwright-testing/) +- [Playwright Python](https://playwright.dev/python/) +- [Browser-Use](https://github.com/browser-use/browser-use) + From c952c5ad5e06d60efa61c2780997bd15a9112ab0 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:12:15 +0530 Subject: [PATCH 22/24] Add requirements.txt for Python sample dependencies --- samples/cdp-tests/requirements.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 samples/cdp-tests/requirements.txt diff --git a/samples/cdp-tests/requirements.txt b/samples/cdp-tests/requirements.txt new file mode 100644 index 0000000..cad986f --- /dev/null +++ b/samples/cdp-tests/requirements.txt @@ -0,0 +1,14 @@ +# Microsoft Playwright Service - Python Samples Dependencies + +# Core (required for all samples) +aiohttp>=3.8.0 +playwright>=1.40.0 +python-dotenv>=1.0.0 + +# For test_runner.py (testing scenario) +pytest>=7.0.0 +pytest-asyncio>=0.21.0 + +# For browser_use_remote.py (AI agent scenario) +pydantic>=2.0.0 +browser-use>=0.1.0 From 04622a0fda60bb4e0947570b1029017038ffa403 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:17:37 +0530 Subject: [PATCH 23/24] Add headers to CDP connection in connectOverCDPScript --- samples/cdp-tests/connectOverCDPScript.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/cdp-tests/connectOverCDPScript.js b/samples/cdp-tests/connectOverCDPScript.js index 8bfe2a1..bc2707d 100644 --- a/samples/cdp-tests/connectOverCDPScript.js +++ b/samples/cdp-tests/connectOverCDPScript.js @@ -53,7 +53,10 @@ async def main(): # Step 2: Connect to remote browser using Playwright async with async_playwright() as p: - browser = await p.chromium.connect_over_cdp(cdp_url) + browser = await p.chromium.connect_over_cdp( + cdp_url, + headers={"User-Agent": "Chrome-DevTools-Protocol/1.3"} + ) print(f"โœ… Connected to remote browser") # Step 3: Use the browser From 4ec7294d007e48e6160cb1b2c92d1ca527f565b6 Mon Sep 17 00:00:00 2001 From: Ishita Jinturkar <88144855+Ishita-Jinturkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:18:03 +0530 Subject: [PATCH 24/24] Add User-Agent header to CDP connection --- samples/cdp-tests/test_runner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/cdp-tests/test_runner.py b/samples/cdp-tests/test_runner.py index 1138aa2..c36480e 100644 --- a/samples/cdp-tests/test_runner.py +++ b/samples/cdp-tests/test_runner.py @@ -63,7 +63,10 @@ async def remote_browser() -> AsyncGenerator[Browser, None]: """ cdp_url = await get_cdp_endpoint() async with async_playwright() as p: - browser = await p.chromium.connect_over_cdp(cdp_url) + browser = await p.chromium.connect_over_cdp( + cdp_url, + headers={"User-Agent": "Chrome-DevTools-Protocol/1.3"} + ) try: yield browser finally: