Skip to content
Merged
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
38 changes: 37 additions & 1 deletion examples/cloud_browser.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import asyncio

from narada import Narada
from narada.window import RemoteBrowserWindow


async def main() -> None:
# Initialize the Narada client.
async with Narada() as narada:
# Open a cloud browser window and initialize the Narada UI agent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol the periods at the end of these comments is so funny

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opus knows me

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
)

# Run a task in this browser window
# Run a task in this browser window.
response = await window.agent(
prompt=(
'Search for "LLM Compiler" on Google and open the first arXiv paper on the results '
Expand All @@ -20,6 +23,39 @@ async def main() -> None:

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
browser_window_id = window.browser_window_id

# Change these to test the different options below.
stop_session_now = False

# The cloud session runs independently. If you want to stop it after the task is
# complete, you can explicitly close it. The session will also auto-expire after the
# configured session_timeout.
if stop_session_now:
print(
f"Stopping cloud session {cloud_browser_session_id} through original window"
)
await window.close()
else:
# Create a `RemoteBrowserWindow` instance with the session ID to manage the session later.
print(
f"Stopping cloud session {cloud_browser_session_id} through RemoteBrowserWindow"
)
remote_window = RemoteBrowserWindow(
cloud_browser_session_id=cloud_browser_session_id,
browser_window_id=browser_window_id,
)
await remote_window.close() # This will stop the cloud session.

############################################################################
# IMPORTANT: The cloud browser continues accruing costs until the session #
# is stopped or times out. To avoid unexpected costs, make sure to stop #
# the session when you're done. #
############################################################################


if __name__ == "__main__":
asyncio.run(main())
24 changes: 23 additions & 1 deletion examples/remote_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,29 @@ async def main() -> None:
# https://app.narada.ai/initialize.
browser_window_id = "REPLACE_WITH_BROWSER_WINDOW_ID"

window = RemoteBrowserWindow(browser_window_id=browser_window_id)
# Optional: If the window was launched as a cloud browser session, provide its session ID to
# enable additional management capabilities such as stopping the session:
#
# ```
# win_1 = await narada.open_and_initialize_cloud_browser_window(...)
#
# browser_window_id = win_1.browser_window_id
# cloud_browser_session_id = win_1.cloud_browser_session_id
#
# ...
#
# win_2 = RemoteBrowserWindow(
# browser_window_id=browser_window_id,
# loud_browser_session_id=cloud_browser_session_id,
# )
# await win_2.close() # This will stop the cloud session.
# ```
cloud_browser_session_id = None

window = RemoteBrowserWindow(
browser_window_id=browser_window_id,
cloud_browser_session_id=cloud_browser_session_id,
)

# Run a task on another machine.
response = await window.agent(
Expand Down
2 changes: 1 addition & 1 deletion packages/narada/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "narada"
version = "0.1.33a13"
version = "0.1.33a14"
description = "Python client SDK for Narada"
license = "Apache-2.0"
readme = "README.md"
Expand Down
12 changes: 1 addition & 11 deletions packages/narada/src/narada/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ class Narada:
_console: Console
_playwright_context_manager: PlaywrightContextManager | None = None
_playwright: Playwright | None = None
_cloud_windows: set[CloudBrowserWindow]

def __init__(
self,
Expand All @@ -74,7 +73,6 @@ def __init__(
api_key = api_key or os.environ["NARADA_API_KEY"]
self._auth_headers = {"x-api-key": api_key}
self._console = Console()
self._cloud_windows = set()

async def __aenter__(self) -> Narada:
await self._validate_sdk_config()
Expand All @@ -84,11 +82,6 @@ async def __aenter__(self) -> Narada:
return self

async def __aexit__(self, *args: Any) -> None:
async with asyncio.TaskGroup() as tg:
for cloud_window in self._cloud_windows:
tg.create_task(cloud_window.cleanup())
self._cloud_windows.clear()

if self._playwright_context_manager is None:
return

Expand Down Expand Up @@ -210,7 +203,7 @@ async def open_and_initialize_cloud_browser_window(
async with cleanup_session.post(
f"{base_url}/cloud-browser/stop-cloud-browser-session",
headers=self._auth_headers,
json={"session_id": session_id},
json={"session_id": session_id, "status": "failed"},
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.ok:
Expand Down Expand Up @@ -282,9 +275,6 @@ async def _initialize_cloud_browser_window(
auth_headers=self._auth_headers,
)

# Track the window for cleanup in __aexit__
self._cloud_windows.add(cloud_window)

if config.interactive:
self._print_success_message(browser_window_id)

Expand Down
100 changes: 77 additions & 23 deletions packages/narada/src/narada/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC
from http import HTTPStatus
from pathlib import Path
from typing import IO, Any, TypeVar, overload
from typing import IO, Any, TypeVar, overload, override

import aiohttp
from narada_core.actions.models import (
Expand Down Expand Up @@ -603,6 +603,7 @@ def __init__(
self,
*,
browser_window_id: str,
cloud_browser_session_id: str | None = None,
api_key: str | None = None,
auth_headers: dict[str, str] | None = None,
) -> None:
Expand All @@ -615,6 +616,28 @@ def __init__(
base_url=base_url,
browser_window_id=browser_window_id,
)
self._cloud_browser_session_id = cloud_browser_session_id

@property
def cloud_browser_session_id(self) -> str | None:
return self._cloud_browser_session_id

@override
async def close(self, *, timeout: int | None = None) -> None:
"""Closes the browser window.

If this window is backed by a cloud browser session, this also stops the cloud
session.
"""
if self._cloud_browser_session_id is None:
return await super().close(timeout=timeout)

await _stop_cloud_browser_session(
base_url=self._base_url,
auth_headers=self._auth_headers,
session_id=self._cloud_browser_session_id,
timeout=timeout,
)

def __str__(self) -> str:
return f"RemoteBrowserWindow(browser_window_id={self.browser_window_id})"
Expand Down Expand Up @@ -646,28 +669,59 @@ def __init__(
)
self._session_id = session_id

async def cleanup(self) -> None:
"""Stop the cloud browser session."""
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self._base_url}/cloud-browser/stop-cloud-browser-session",
headers=self._auth_headers,
json={
"session_id": self._session_id,
},
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.ok:
response_data = await resp.json()
if not response_data.get("success"):
logger.warning(
f"Failed to stop session: {response_data.get('message')}"
)
else:
logger.warning(f"Failed to stop session: {resp.status}")
except Exception as e:
logger.warning(f"Error calling stop session endpoint: {e}")
@property
def cloud_browser_session_id(self) -> str:
return self._session_id

@override
async def close(self, *, timeout: int | None = None) -> None:
"""Stops the cloud browser session.

Unlike local browser windows where close() closes a single window, this stops the
entire cloud session since the serverless container manages the browser lifecycle.
"""
await _stop_cloud_browser_session(
base_url=self._base_url,
auth_headers=self._auth_headers,
session_id=self._session_id,
timeout=timeout,
)

def __str__(self) -> str:
return (
"CloudBrowserWindow("
f"cloud_browser_session_id={self._session_id}, "
f"browser_window_id={self.browser_window_id}"
")"
)


async def _stop_cloud_browser_session(
*,
base_url: str,
auth_headers: dict[str, str],
session_id: str,
timeout: int | None = None,
) -> None:
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{base_url}/cloud-browser/stop-cloud-browser-session",
headers=auth_headers,
json={"session_id": session_id},
timeout=aiohttp.ClientTimeout(total=timeout or 10),
) as resp:
if resp.ok:
response_data = await resp.json()
if not response_data.get("success"):
logger.warning(
"Failed to stop session: %s",
response_data.get("message"),
)
else:
logger.warning("Failed to stop session: %s", resp.status)
except Exception as e:
logger.warning("Error calling stop session endpoint: %s", e)


def create_side_panel_url(config: BrowserConfig, browser_window_id: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.