narada 0.1.32__py3-none-any.whl → 0.1.33a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
narada/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@ from narada.client import Narada
|
|
|
2
2
|
from narada.config import BrowserConfig, ProxyConfig
|
|
3
3
|
from narada.utils import download_file, render_html
|
|
4
4
|
from narada.version import __version__
|
|
5
|
-
from narada.window import LocalBrowserWindow, RemoteBrowserWindow
|
|
5
|
+
from narada.window import CloudBrowserWindow, LocalBrowserWindow, RemoteBrowserWindow
|
|
6
6
|
from narada_core.errors import (
|
|
7
7
|
NaradaError,
|
|
8
8
|
NaradaExtensionMissingError,
|
|
@@ -17,6 +17,7 @@ __all__ = [
|
|
|
17
17
|
"__version__",
|
|
18
18
|
"Agent",
|
|
19
19
|
"BrowserConfig",
|
|
20
|
+
"CloudBrowserWindow",
|
|
20
21
|
"download_file",
|
|
21
22
|
"File",
|
|
22
23
|
"LocalBrowserWindow",
|
narada/client.py
CHANGED
|
@@ -14,7 +14,11 @@ import semver
|
|
|
14
14
|
from narada.config import BrowserConfig, ProxyConfig
|
|
15
15
|
from narada.utils import assert_never
|
|
16
16
|
from narada.version import __version__
|
|
17
|
-
from narada.window import
|
|
17
|
+
from narada.window import (
|
|
18
|
+
LocalBrowserWindow,
|
|
19
|
+
CloudBrowserWindow,
|
|
20
|
+
create_side_panel_url,
|
|
21
|
+
)
|
|
18
22
|
from narada_core.errors import (
|
|
19
23
|
NaradaExtensionMissingError,
|
|
20
24
|
NaradaExtensionUnauthenticatedError,
|
|
@@ -57,10 +61,12 @@ class Narada:
|
|
|
57
61
|
_console: Console
|
|
58
62
|
_playwright_context_manager: PlaywrightContextManager | None = None
|
|
59
63
|
_playwright: Playwright | None = None
|
|
64
|
+
_cloud_windows: set[CloudBrowserWindow]
|
|
60
65
|
|
|
61
66
|
def __init__(self, *, api_key: str | None = None) -> None:
|
|
62
67
|
self._api_key = api_key or os.environ["NARADA_API_KEY"]
|
|
63
68
|
self._console = Console()
|
|
69
|
+
self._cloud_windows = set()
|
|
64
70
|
|
|
65
71
|
async def __aenter__(self) -> Narada:
|
|
66
72
|
await self._validate_sdk_config()
|
|
@@ -70,6 +76,11 @@ class Narada:
|
|
|
70
76
|
return self
|
|
71
77
|
|
|
72
78
|
async def __aexit__(self, *args: Any) -> None:
|
|
79
|
+
async with asyncio.TaskGroup() as tg:
|
|
80
|
+
for cloud_window in self._cloud_windows:
|
|
81
|
+
tg.create_task(cloud_window.cleanup())
|
|
82
|
+
self._cloud_windows.clear()
|
|
83
|
+
|
|
73
84
|
if self._playwright_context_manager is None:
|
|
74
85
|
return
|
|
75
86
|
|
|
@@ -133,6 +144,121 @@ class Narada:
|
|
|
133
144
|
context=side_panel_page.context,
|
|
134
145
|
)
|
|
135
146
|
|
|
147
|
+
async def open_and_initialize_cloud_browser_window(
|
|
148
|
+
self,
|
|
149
|
+
config: BrowserConfig | None = None,
|
|
150
|
+
session_name: str | None = None,
|
|
151
|
+
session_timeout: int | None = None,
|
|
152
|
+
) -> CloudBrowserWindow:
|
|
153
|
+
"""Creates a cloud browser by calling the backend.
|
|
154
|
+
|
|
155
|
+
The backend creates a cloud browser session and returns
|
|
156
|
+
a CDP WebSocket URL. This method connects to it, initializes the extension,
|
|
157
|
+
and returns a CloudBrowserWindow instance.
|
|
158
|
+
"""
|
|
159
|
+
assert self._playwright is not None
|
|
160
|
+
playwright = self._playwright
|
|
161
|
+
|
|
162
|
+
config = config or BrowserConfig()
|
|
163
|
+
base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2")
|
|
164
|
+
request_body = {
|
|
165
|
+
"session_name": session_name,
|
|
166
|
+
"session_timeout": session_timeout,
|
|
167
|
+
}
|
|
168
|
+
endpoint_url = f"{base_url}/cloud-browser/create-cloud-browser-session"
|
|
169
|
+
|
|
170
|
+
async with aiohttp.ClientSession() as session:
|
|
171
|
+
async with session.post(
|
|
172
|
+
endpoint_url,
|
|
173
|
+
headers={"x-api-key": self._api_key},
|
|
174
|
+
json=request_body,
|
|
175
|
+
timeout=aiohttp.ClientTimeout(
|
|
176
|
+
total=180
|
|
177
|
+
), # 3 minutes for session startup
|
|
178
|
+
) as resp:
|
|
179
|
+
if not resp.ok:
|
|
180
|
+
error_text = await resp.text()
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
f"Failed to create cloud browser session: {resp.status} {error_text}\n"
|
|
183
|
+
f"Endpoint URL: {endpoint_url}"
|
|
184
|
+
)
|
|
185
|
+
response_data = await resp.json()
|
|
186
|
+
|
|
187
|
+
cdp_websocket_url = response_data["cdp_websocket_url"]
|
|
188
|
+
session_id = response_data["session_id"]
|
|
189
|
+
login_url = response_data["login_url"]
|
|
190
|
+
cdp_auth_headers = response_data.get("cdp_auth_headers")
|
|
191
|
+
|
|
192
|
+
# Connect to browser via CDP with authentication headers
|
|
193
|
+
try:
|
|
194
|
+
browser = await playwright.chromium.connect_over_cdp(
|
|
195
|
+
cdp_websocket_url, headers=cdp_auth_headers
|
|
196
|
+
)
|
|
197
|
+
except Exception:
|
|
198
|
+
# Clean up the session if CDP connection fails
|
|
199
|
+
try:
|
|
200
|
+
async with aiohttp.ClientSession() as cleanup_session:
|
|
201
|
+
async with cleanup_session.post(
|
|
202
|
+
f"{base_url}/cloud-browser/stop-cloud-browser-session",
|
|
203
|
+
headers={"x-api-key": self._api_key},
|
|
204
|
+
json={"session_id": session_id},
|
|
205
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
206
|
+
) as resp:
|
|
207
|
+
if resp.ok:
|
|
208
|
+
logging.info(
|
|
209
|
+
f"Cleaned up session {session_id} after CDP connection failure"
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
logging.warning(
|
|
213
|
+
f"Failed to cleanup session {session_id}: {resp.status}"
|
|
214
|
+
)
|
|
215
|
+
except Exception as cleanup_error:
|
|
216
|
+
logging.warning(
|
|
217
|
+
f"Error cleaning up session {session_id}: {cleanup_error}"
|
|
218
|
+
)
|
|
219
|
+
# Re-raise the original connection error
|
|
220
|
+
raise
|
|
221
|
+
context = (
|
|
222
|
+
browser.contexts[0] if browser.contexts else await browser.new_context()
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Navigate to login URL (provided by backend with custom token)
|
|
226
|
+
initialization_page = await context.new_page()
|
|
227
|
+
await initialization_page.goto(
|
|
228
|
+
login_url, wait_until="domcontentloaded", timeout=60_000
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Wait for sign-in to process to complete.
|
|
232
|
+
await asyncio.sleep(15) # TODO: improve it in the future
|
|
233
|
+
await initialization_page.reload(wait_until="domcontentloaded", timeout=60_000)
|
|
234
|
+
|
|
235
|
+
# Wait for browser window ID
|
|
236
|
+
browser_window_id = await self._wait_for_browser_window_id(
|
|
237
|
+
initialization_page, config
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# TODO: consider this
|
|
241
|
+
# Get side panel page
|
|
242
|
+
# side_panel_url = create_side_panel_url(config, browser_window_id)
|
|
243
|
+
# side_panel_page = next(
|
|
244
|
+
# (p for p in context.pages if p.url == side_panel_url), None
|
|
245
|
+
# )
|
|
246
|
+
# await self._fix_download_behavior(side_panel_page)
|
|
247
|
+
|
|
248
|
+
cloud_window = CloudBrowserWindow(
|
|
249
|
+
browser_window_id=browser_window_id,
|
|
250
|
+
session_id=session_id,
|
|
251
|
+
api_key=self._api_key,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Track the window for cleanup in __aexit__
|
|
255
|
+
self._cloud_windows.add(cloud_window)
|
|
256
|
+
|
|
257
|
+
if config.interactive:
|
|
258
|
+
self._print_success_message(browser_window_id)
|
|
259
|
+
|
|
260
|
+
return cloud_window
|
|
261
|
+
|
|
136
262
|
async def initialize_in_existing_browser_window(
|
|
137
263
|
self, config: BrowserConfig | None = None
|
|
138
264
|
) -> LocalBrowserWindow:
|
narada/window.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
2
3
|
import os
|
|
3
4
|
import time
|
|
4
5
|
from abc import ABC
|
|
@@ -47,9 +48,13 @@ from narada_core.models import (
|
|
|
47
48
|
Response,
|
|
48
49
|
UserResourceCredentials,
|
|
49
50
|
)
|
|
50
|
-
from playwright.async_api import
|
|
51
|
+
from playwright.async_api import (
|
|
52
|
+
BrowserContext,
|
|
53
|
+
)
|
|
51
54
|
from pydantic import BaseModel
|
|
52
55
|
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
53
58
|
_StructuredOutput = TypeVar("_StructuredOutput", bound=BaseModel)
|
|
54
59
|
|
|
55
60
|
|
|
@@ -552,9 +557,10 @@ class LocalBrowserWindow(BaseBrowserWindow):
|
|
|
552
557
|
config: BrowserConfig,
|
|
553
558
|
context: BrowserContext,
|
|
554
559
|
) -> None:
|
|
560
|
+
base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2")
|
|
555
561
|
super().__init__(
|
|
556
562
|
api_key=api_key,
|
|
557
|
-
base_url=
|
|
563
|
+
base_url=base_url,
|
|
558
564
|
browser_window_id=browser_window_id,
|
|
559
565
|
)
|
|
560
566
|
self._browser_process_id = browser_process_id
|
|
@@ -586,9 +592,10 @@ class LocalBrowserWindow(BaseBrowserWindow):
|
|
|
586
592
|
|
|
587
593
|
class RemoteBrowserWindow(BaseBrowserWindow):
|
|
588
594
|
def __init__(self, *, browser_window_id: str, api_key: str | None = None) -> None:
|
|
595
|
+
base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2")
|
|
589
596
|
super().__init__(
|
|
590
597
|
api_key=api_key or os.environ["NARADA_API_KEY"],
|
|
591
|
-
base_url=
|
|
598
|
+
base_url=base_url,
|
|
592
599
|
browser_window_id=browser_window_id,
|
|
593
600
|
)
|
|
594
601
|
|
|
@@ -596,5 +603,51 @@ class RemoteBrowserWindow(BaseBrowserWindow):
|
|
|
596
603
|
return f"RemoteBrowserWindow(browser_window_id={self.browser_window_id})"
|
|
597
604
|
|
|
598
605
|
|
|
606
|
+
class CloudBrowserWindow(BaseBrowserWindow):
|
|
607
|
+
"""A browser window that connects to a backend-cloud browser session via CDP.
|
|
608
|
+
|
|
609
|
+
This class connects to a cloud browser session created by the backend API and provides
|
|
610
|
+
the same interface as other browser window classes for agent operations.
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
def __init__(
|
|
614
|
+
self,
|
|
615
|
+
*,
|
|
616
|
+
browser_window_id: str,
|
|
617
|
+
session_id: str,
|
|
618
|
+
api_key: str | None = None,
|
|
619
|
+
) -> None:
|
|
620
|
+
base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2")
|
|
621
|
+
super().__init__(
|
|
622
|
+
api_key=api_key or os.environ["NARADA_API_KEY"],
|
|
623
|
+
base_url=base_url,
|
|
624
|
+
browser_window_id=browser_window_id,
|
|
625
|
+
)
|
|
626
|
+
self._session_id = session_id
|
|
627
|
+
|
|
628
|
+
async def cleanup(self) -> None:
|
|
629
|
+
"""Stop the cloud browser session."""
|
|
630
|
+
try:
|
|
631
|
+
async with aiohttp.ClientSession() as session:
|
|
632
|
+
async with session.post(
|
|
633
|
+
f"{self._base_url}/cloud-browser/stop-cloud-browser-session",
|
|
634
|
+
headers={"x-api-key": self._api_key},
|
|
635
|
+
json={
|
|
636
|
+
"session_id": self._session_id,
|
|
637
|
+
},
|
|
638
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
639
|
+
) as resp:
|
|
640
|
+
if resp.ok:
|
|
641
|
+
response_data = await resp.json()
|
|
642
|
+
if not response_data.get("success"):
|
|
643
|
+
logger.warning(
|
|
644
|
+
f"Failed to stop session: {response_data.get('message')}"
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
logger.warning(f"Failed to stop session: {resp.status}")
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.warning(f"Error calling stop session endpoint: {e}")
|
|
650
|
+
|
|
651
|
+
|
|
599
652
|
def create_side_panel_url(config: BrowserConfig, browser_window_id: str) -> str:
|
|
600
653
|
return f"chrome-extension://{config.extension_id}/sidepanel.html?browserWindowId={browser_window_id}"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
narada/__init__.py,sha256=gVa9HUFTeHot7VR2XPtCEsVeRiK7MDJJvKU1eEhLqXQ,1013
|
|
2
|
+
narada/client.py,sha256=Ea-zyJo6rZCQNi1higShelILkR9FLpNrtYoFse7heUA,26630
|
|
3
|
+
narada/config.py,sha256=S0B8GNd-0td_69oKaPN60WAq_ODeYU7avE_KAxN5vCg,3052
|
|
4
|
+
narada/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
narada/utils.py,sha256=gdLwNMXPpRohDcIIe0cB3KhvZ8X1QfAlKVh1sXWeJmk,1284
|
|
6
|
+
narada/version.py,sha256=kwW6yy0_4Pf3kt888eeCG0VwBb2L2rCkrkpdZEC_3rA,193
|
|
7
|
+
narada/window.py,sha256=e3pXn-wQMKm8QRvtAKfep0iGQE2LP9doP85z1t6VVpI,23036
|
|
8
|
+
narada-0.1.33a1.dist-info/METADATA,sha256=e3h7vEqdOHWEedcZw83trCPHVUPUry3FiE8gWu1aifM,5149
|
|
9
|
+
narada-0.1.33a1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
narada-0.1.33a1.dist-info/RECORD,,
|
narada-0.1.32.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
narada/__init__.py,sha256=HPaTpe3-kHtuFxtdADlFTaPWTyZx5RaKdbt9o9YpTYE,967
|
|
2
|
-
narada/client.py,sha256=GHz4iFsQmvPMfes1gBED6sSdi0tdIpZx6ttFNSeRpFg,21649
|
|
3
|
-
narada/config.py,sha256=S0B8GNd-0td_69oKaPN60WAq_ODeYU7avE_KAxN5vCg,3052
|
|
4
|
-
narada/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
narada/utils.py,sha256=gdLwNMXPpRohDcIIe0cB3KhvZ8X1QfAlKVh1sXWeJmk,1284
|
|
6
|
-
narada/version.py,sha256=kwW6yy0_4Pf3kt888eeCG0VwBb2L2rCkrkpdZEC_3rA,193
|
|
7
|
-
narada/window.py,sha256=YpIiukGcSVGimLB1sJIUII1Nte29YfvoiOp2SUVldZE,21104
|
|
8
|
-
narada-0.1.32.dist-info/METADATA,sha256=XrG2QDIePHLKCuSwAx5GHNceWQ02LD6WZCdVjf4Mbgs,5147
|
|
9
|
-
narada-0.1.32.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
-
narada-0.1.32.dist-info/RECORD,,
|
|
File without changes
|