Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ wheels/

.DS_Store
*.*~

# Cloud downloads
cloud_downloads/
4 changes: 3 additions & 1 deletion examples/cloud_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async def main() -> None:
# Open a cloud browser window and initialize the Narada UI agent.
window = await narada.open_and_initialize_cloud_browser_window(
session_name="my-cloud-browser-session", # Optional: label the session
session_timeout=3600, # Optional: session timeout in seconds
session_timeout=1800, # Optional: timeout in seconds, up to 8 hours, default is 30 minutes
)

# Run a task in this browser window.
Expand All @@ -21,8 +21,10 @@ async def main() -> None:
)
)

# All downloads are placed in the narada-python-sdk/cloud_downloads directory.
print("Response:", response.model_dump_json(indent=2))


# The cloud session is still running after exiting the context manager.
# You can save the session ID for later reconnection or management.
cloud_browser_session_id = window.cloud_browser_session_id
Expand Down
2 changes: 2 additions & 0 deletions packages/narada/src/narada/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from narada_core.models import Agent, File, Response, ResponseContent

from narada.client import Narada
from narada.cloud_downloads import DownloadInfo
from narada.config import BrowserConfig, ProxyConfig
from narada.utils import download_file, render_html
from narada.version import __version__
Expand All @@ -20,6 +21,7 @@
"BrowserConfig",
"CloudBrowserWindow",
"download_file",
"DownloadInfo",
"File",
"LocalBrowserWindow",
"Narada",
Expand Down
51 changes: 45 additions & 6 deletions packages/narada/src/narada/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import concurrent.futures
import logging
import os
import subprocess
Expand All @@ -17,6 +18,12 @@
NaradaTimeoutError,
NaradaUnsupportedBrowserError,
)
from pathlib import Path

from narada.cloud_downloads import (
CDPDownloadHandler,
make_default_on_download_complete_callback,
)
from narada_core.models import _SdkConfig
from packaging.version import Version
from playwright._impl._errors import Error as PlaywrightError
Expand Down Expand Up @@ -73,6 +80,7 @@ def __init__(
api_key = api_key or os.environ["NARADA_API_KEY"]
self._auth_headers = {"x-api-key": api_key}
self._console = Console()
self._pending_download_futures: list[concurrent.futures.Future] = []

async def __aenter__(self) -> Narada:
await self._validate_sdk_config()
Expand All @@ -85,6 +93,16 @@ async def __aexit__(self, *args: Any) -> None:
if self._playwright_context_manager is None:
return

# Wait for any in-flight download transfers (from default callback) so the
# browser is not closed until transfers finish. Await so the event loop can
# run the transfer coroutines (they were scheduled via run_coroutine_threadsafe).
for fut in self._pending_download_futures:
try:
await asyncio.wait_for(asyncio.wrap_future(fut), timeout=300)
except (asyncio.TimeoutError, Exception):
pass
self._pending_download_futures.clear()

await self._playwright_context_manager.__aexit__(*args)
self._playwright_context_manager = None
self._playwright = None
Expand Down Expand Up @@ -261,18 +279,39 @@ async def _initialize_cloud_browser_window(
logging.info("Waiting for Narada extension to be installed...")
await asyncio.sleep(1)

# TODO: consider this
# TODO: This is a hack
await browser.close()
browser = await self._playwright.chromium.connect_over_cdp(cdp_websocket_url, headers=cdp_auth_headers)
context = browser.contexts[0]
# Get side panel page
# side_panel_url = create_side_panel_url(config, browser_window_id)
# side_panel_page = next(
# (p for p in context.pages if p.url == side_panel_url), None
# )
# await self._fix_download_behavior(side_panel_page)
side_panel_url = create_side_panel_url(config, browser_window_id)
side_panel_page = next(
(p for p in context.pages if p.url == side_panel_url), None
)
await self._fix_download_behavior(side_panel_page)

# Set up browser-level CDP download handler to capture downloads from any tab.
# When no callback is provided, use a default that transfers files to local disk.
on_download_complete = config.on_download_complete
if on_download_complete is None:
loop = asyncio.get_running_loop()
base_dir = Path.cwd() / "cloud_downloads"
on_download_complete = make_default_on_download_complete_callback(
browser, loop, base_dir, self._pending_download_futures
)

download_handler = CDPDownloadHandler(
session_id=session_id,
on_download_complete=on_download_complete,
)
await download_handler.setup(browser)

cloud_window = CloudBrowserWindow(
browser_window_id=browser_window_id,
session_id=session_id,
auth_headers=self._auth_headers,
browser=browser,
download_handler=download_handler,
)

if config.interactive:
Expand Down
Loading
Loading