mirror of https://github.com/kortix-ai/suna.git
fix(browser): multiple tab handling
This commit is contained in:
parent
1fedcbabfa
commit
172e95c248
|
@ -96,11 +96,6 @@ WORKDIR /app
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy server script
|
|
||||||
COPY . /app
|
|
||||||
COPY server.py /app/server.py
|
|
||||||
COPY browser_api.py /app/browser_api.py
|
|
||||||
|
|
||||||
# Install Playwright and browsers with system dependencies
|
# Install Playwright and browsers with system dependencies
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
# Install Playwright package first
|
# Install Playwright package first
|
||||||
|
@ -111,6 +106,11 @@ RUN playwright install chromium
|
||||||
# Verify installation
|
# Verify installation
|
||||||
RUN python -c "from playwright.sync_api import sync_playwright; print('Playwright installation verified')"
|
RUN python -c "from playwright.sync_api import sync_playwright; print('Playwright installation verified')"
|
||||||
|
|
||||||
|
# Copy server script
|
||||||
|
COPY . /app
|
||||||
|
COPY server.py /app/server.py
|
||||||
|
COPY browser_api.py /app/browser_api.py
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV CHROME_PATH=/ms-playwright/chromium-*/chrome-linux/chrome
|
ENV CHROME_PATH=/ms-playwright/chromium-*/chrome-linux/chrome
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import FastAPI, APIRouter, HTTPException, Body
|
from fastapi import FastAPI, APIRouter, HTTPException, Body
|
||||||
from playwright.async_api import async_playwright, Browser, Page
|
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -282,6 +282,7 @@ class BrowserAutomation:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.router = APIRouter()
|
self.router = APIRouter()
|
||||||
self.browser: Browser = None
|
self.browser: Browser = None
|
||||||
|
self.browser_context: BrowserContext = None
|
||||||
self.pages: List[Page] = []
|
self.pages: List[Page] = []
|
||||||
self.current_page_index: int = 0
|
self.current_page_index: int = 0
|
||||||
self.logger = logging.getLogger("browser_automation")
|
self.logger = logging.getLogger("browser_automation")
|
||||||
|
@ -341,6 +342,7 @@ class BrowserAutomation:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.browser = await playwright.chromium.launch(**launch_options)
|
self.browser = await playwright.chromium.launch(**launch_options)
|
||||||
|
self.browser_context = await self.browser.new_context(viewport={'width': 1024, 'height': 768})
|
||||||
print("Browser launched successfully")
|
print("Browser launched successfully")
|
||||||
except Exception as browser_error:
|
except Exception as browser_error:
|
||||||
print(f"Failed to launch browser: {browser_error}")
|
print(f"Failed to launch browser: {browser_error}")
|
||||||
|
@ -348,6 +350,7 @@ class BrowserAutomation:
|
||||||
print("Retrying with minimal options...")
|
print("Retrying with minimal options...")
|
||||||
launch_options = {"timeout": 90000}
|
launch_options = {"timeout": 90000}
|
||||||
self.browser = await playwright.chromium.launch(**launch_options)
|
self.browser = await playwright.chromium.launch(**launch_options)
|
||||||
|
self.browser_context = await self.browser.new_context(viewport={'width': 1024, 'height': 768})
|
||||||
print("Browser launched with minimal options")
|
print("Browser launched with minimal options")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -356,13 +359,20 @@ class BrowserAutomation:
|
||||||
self.current_page_index = 0
|
self.current_page_index = 0
|
||||||
except Exception as page_error:
|
except Exception as page_error:
|
||||||
print(f"Error finding existing page, creating new one. ( {page_error})")
|
print(f"Error finding existing page, creating new one. ( {page_error})")
|
||||||
page = await self.browser.new_page(viewport={'width': 1024, 'height': 768})
|
page = await self.browser_context.new_page()
|
||||||
print("New page created successfully")
|
print("New page created successfully")
|
||||||
self.pages.append(page)
|
self.pages.append(page)
|
||||||
self.current_page_index = 0
|
self.current_page_index = 0
|
||||||
# Navigate directly to google.com instead of about:blank
|
# Navigate directly to google.com instead of about:blank
|
||||||
await page.goto("https://www.google.com", wait_until="domcontentloaded", timeout=30000)
|
await page.goto("https://www.google.com", wait_until="domcontentloaded", timeout=30000)
|
||||||
print("Navigated to google.com")
|
print("Navigated to google.com")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.browser_context.on("page", self.handle_page_created)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting up page event handler: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
print("Browser initialization completed successfully")
|
print("Browser initialization completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -372,8 +382,17 @@ class BrowserAutomation:
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
"""Clean up browser instance on shutdown"""
|
"""Clean up browser instance on shutdown"""
|
||||||
|
if self.browser_context:
|
||||||
|
await self.browser_context.close()
|
||||||
if self.browser:
|
if self.browser:
|
||||||
await self.browser.close()
|
await self.browser.close()
|
||||||
|
|
||||||
|
async def handle_page_created(self, page: Page):
|
||||||
|
"""Handle new page creation"""
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
self.pages.append(page)
|
||||||
|
self.current_page_index = len(self.pages) - 1
|
||||||
|
print(f"Page created: {page.url}; current page index: {self.current_page_index}")
|
||||||
|
|
||||||
async def get_current_page(self) -> Page:
|
async def get_current_page(self) -> Page:
|
||||||
"""Get the current active page"""
|
"""Get the current active page"""
|
||||||
|
@ -958,6 +977,7 @@ class BrowserAutomation:
|
||||||
# Give time for any navigation or DOM updates to occur
|
# Give time for any navigation or DOM updates to occur
|
||||||
await page.wait_for_load_state("networkidle", timeout=5000)
|
await page.wait_for_load_state("networkidle", timeout=5000)
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
# Get updated state after action
|
# Get updated state after action
|
||||||
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"click_coordinates({action.x}, {action.y})")
|
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"click_coordinates({action.x}, {action.y})")
|
||||||
|
|
||||||
|
@ -977,6 +997,7 @@ class BrowserAutomation:
|
||||||
|
|
||||||
# Try to get state even after error
|
# Try to get state even after error
|
||||||
try:
|
try:
|
||||||
|
await asyncio.sleep(1)
|
||||||
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state("click_coordinates_error_recovery")
|
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state("click_coordinates_error_recovery")
|
||||||
return self.build_action_result(
|
return self.build_action_result(
|
||||||
False,
|
False,
|
||||||
|
@ -1076,7 +1097,7 @@ class BrowserAutomation:
|
||||||
await page.wait_for_load_state("networkidle", timeout=5000)
|
await page.wait_for_load_state("networkidle", timeout=5000)
|
||||||
except Exception as wait_error:
|
except Exception as wait_error:
|
||||||
print(f"Timeout or error waiting for network idle after click: {wait_error}")
|
print(f"Timeout or error waiting for network idle after click: {wait_error}")
|
||||||
await asyncio.sleep(1) # Fallback wait
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# Get updated state after action
|
# Get updated state after action
|
||||||
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"click_element({action.index})")
|
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"click_element({action.index})")
|
||||||
|
@ -1161,6 +1182,7 @@ class BrowserAutomation:
|
||||||
# Fallback to xpath
|
# Fallback to xpath
|
||||||
await page.fill(f"//{element.tag_name}[{action.index}]", action.text)
|
await page.fill(f"//{element.tag_name}[{action.index}]", action.text)
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
# Get updated state after action
|
# Get updated state after action
|
||||||
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"input_text({action.index}, '{action.text}')")
|
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"input_text({action.index}, '{action.text}')")
|
||||||
|
|
||||||
|
@ -1192,6 +1214,7 @@ class BrowserAutomation:
|
||||||
page = await self.get_current_page()
|
page = await self.get_current_page()
|
||||||
await page.keyboard.press(action.keys)
|
await page.keyboard.press(action.keys)
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
# Get updated state after action
|
# Get updated state after action
|
||||||
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"send_keys({action.keys})")
|
dom_state, screenshot, elements, metadata = await self.get_updated_browser_state(f"send_keys({action.keys})")
|
||||||
|
|
||||||
|
@ -1267,7 +1290,7 @@ class BrowserAutomation:
|
||||||
try:
|
try:
|
||||||
print(f"Attempting to open new tab with URL: {action.url}")
|
print(f"Attempting to open new tab with URL: {action.url}")
|
||||||
# Create new page in same browser instance
|
# Create new page in same browser instance
|
||||||
new_page = await self.browser.new_page()
|
new_page = await self.browser_context.new_page()
|
||||||
print(f"New page created successfully")
|
print(f"New page created successfully")
|
||||||
|
|
||||||
# Navigate to the URL
|
# Navigate to the URL
|
||||||
|
|
|
@ -159,7 +159,7 @@ class Configuration:
|
||||||
STRIPE_PRODUCT_ID_STAGING: str = 'prod_SCgIj3G7yPOAWY'
|
STRIPE_PRODUCT_ID_STAGING: str = 'prod_SCgIj3G7yPOAWY'
|
||||||
|
|
||||||
# Sandbox configuration
|
# Sandbox configuration
|
||||||
SANDBOX_IMAGE_NAME = "kortix/suna:0.1.2.8"
|
SANDBOX_IMAGE_NAME = "tnfssc/suna-sandbox:beta"
|
||||||
SANDBOX_ENTRYPOINT = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf"
|
SANDBOX_ENTRYPOINT = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf"
|
||||||
|
|
||||||
# LangFuse configuration
|
# LangFuse configuration
|
||||||
|
|
Loading…
Reference in New Issue