narada 0.1.28__py3-none-any.whl → 0.1.29__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 +2 -1
- narada/client.py +120 -5
- narada/config.py +45 -0
- narada/window.py +40 -3
- {narada-0.1.28.dist-info → narada-0.1.29.dist-info}/METADATA +2 -2
- narada-0.1.29.dist-info/RECORD +10 -0
- narada-0.1.28.dist-info/RECORD +0 -10
- {narada-0.1.28.dist-info → narada-0.1.29.dist-info}/WHEEL +0 -0
narada/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from narada_core.errors import (
|
|
|
9
9
|
from narada_core.models import Agent, File, Response, ResponseContent
|
|
10
10
|
|
|
11
11
|
from narada.client import Narada
|
|
12
|
-
from narada.config import BrowserConfig
|
|
12
|
+
from narada.config import BrowserConfig, ProxyConfig
|
|
13
13
|
from narada.utils import download_file, render_html
|
|
14
14
|
from narada.version import __version__
|
|
15
15
|
from narada.window import LocalBrowserWindow, RemoteBrowserWindow
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"NaradaInitializationError",
|
|
29
29
|
"NaradaTimeoutError",
|
|
30
30
|
"NaradaUnsupportedBrowserError",
|
|
31
|
+
"ProxyConfig",
|
|
31
32
|
"RemoteBrowserWindow",
|
|
32
33
|
"render_html",
|
|
33
34
|
"Response",
|
narada/client.py
CHANGED
|
@@ -21,6 +21,8 @@ from narada_core.errors import (
|
|
|
21
21
|
from narada_core.models import _SdkConfig
|
|
22
22
|
from playwright._impl._errors import Error as PlaywrightError
|
|
23
23
|
from playwright.async_api import (
|
|
24
|
+
Browser,
|
|
25
|
+
CDPSession,
|
|
24
26
|
ElementHandle,
|
|
25
27
|
Page,
|
|
26
28
|
Playwright,
|
|
@@ -32,7 +34,7 @@ from playwright.async_api import (
|
|
|
32
34
|
from playwright.async_api._context_manager import PlaywrightContextManager
|
|
33
35
|
from rich.console import Console
|
|
34
36
|
|
|
35
|
-
from narada.config import BrowserConfig
|
|
37
|
+
from narada.config import BrowserConfig, ProxyConfig
|
|
36
38
|
from narada.utils import assert_never
|
|
37
39
|
from narada.version import __version__
|
|
38
40
|
from narada.window import LocalBrowserWindow, create_side_panel_url
|
|
@@ -146,6 +148,13 @@ class Narada:
|
|
|
146
148
|
|
|
147
149
|
config = config or BrowserConfig()
|
|
148
150
|
|
|
151
|
+
if config.proxy is not None:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"Proxy configuration is not supported for `initialize_in_existing_browser_window`. "
|
|
154
|
+
"Proxy settings must be specified when launching Chrome. "
|
|
155
|
+
"Use `open_and_initialize_browser_window` instead."
|
|
156
|
+
)
|
|
157
|
+
|
|
149
158
|
browser = await playwright.chromium.connect_over_cdp(config.cdp_url)
|
|
150
159
|
|
|
151
160
|
# Generate a unique tag for the initialization URL
|
|
@@ -193,6 +202,13 @@ class Narada:
|
|
|
193
202
|
window_tag = uuid4().hex
|
|
194
203
|
tagged_initialization_url = f"{config.initialization_url}?t={window_tag}"
|
|
195
204
|
|
|
205
|
+
# When proxy auth is needed, launch with about:blank to avoid Chrome's startup auth prompt.
|
|
206
|
+
# We'll set up the CDP auth handler and then navigate to the init URL.
|
|
207
|
+
proxy_requires_auth = (
|
|
208
|
+
config.proxy is not None and config.proxy.requires_authentication
|
|
209
|
+
)
|
|
210
|
+
launch_url = "about:blank" if proxy_requires_auth else tagged_initialization_url
|
|
211
|
+
|
|
196
212
|
browser_args = [
|
|
197
213
|
f"--user-data-dir={config.user_data_dir}",
|
|
198
214
|
f"--profile-directory={config.profile_directory}",
|
|
@@ -200,11 +216,20 @@ class Narada:
|
|
|
200
216
|
"--no-default-browser-check",
|
|
201
217
|
"--no-first-run",
|
|
202
218
|
"--new-window",
|
|
203
|
-
|
|
204
|
-
# TODO: This is needed if we don't use CDP but let Playwright manage the browser.
|
|
205
|
-
# "--disable-blink-features=AutomationControlled",
|
|
219
|
+
launch_url,
|
|
206
220
|
]
|
|
207
221
|
|
|
222
|
+
# Add proxy arguments if configured.
|
|
223
|
+
if config.proxy is not None:
|
|
224
|
+
config.proxy.validate()
|
|
225
|
+
browser_args.append(f"--proxy-server={config.proxy.server}")
|
|
226
|
+
|
|
227
|
+
if config.proxy.bypass:
|
|
228
|
+
browser_args.append(f"--proxy-bypass-list={config.proxy.bypass}")
|
|
229
|
+
|
|
230
|
+
if config.proxy.ignore_cert_errors:
|
|
231
|
+
browser_args.append("--ignore-certificate-errors")
|
|
232
|
+
|
|
208
233
|
# Launch an independent browser process which will not be killed when the current program
|
|
209
234
|
# exits.
|
|
210
235
|
if sys.platform == "win32":
|
|
@@ -235,6 +260,14 @@ class Narada:
|
|
|
235
260
|
browser_window_id = None
|
|
236
261
|
side_panel_page = None
|
|
237
262
|
max_cdp_connect_attempts = 10
|
|
263
|
+
|
|
264
|
+
# Track whether we've already navigated from about:blank to the initialization URL.
|
|
265
|
+
# This is only relevant when proxy auth is enabled, where we launch with about:blank
|
|
266
|
+
# to set up CDP auth handlers before any network traffic. We must only navigate once,
|
|
267
|
+
# because on retry iterations context.pages[0] could be any page (side panel, devtools,
|
|
268
|
+
# etc.) and navigating it would break the initialization flow.
|
|
269
|
+
did_initial_navigation = False
|
|
270
|
+
|
|
238
271
|
for attempt in range(max_cdp_connect_attempts):
|
|
239
272
|
try:
|
|
240
273
|
browser = await playwright.chromium.connect_over_cdp(config.cdp_url)
|
|
@@ -246,8 +279,23 @@ class Narada:
|
|
|
246
279
|
await asyncio.sleep(2)
|
|
247
280
|
continue
|
|
248
281
|
|
|
249
|
-
# Grab the browser window ID from the page we just opened.
|
|
250
282
|
context = browser.contexts[0]
|
|
283
|
+
|
|
284
|
+
# If proxy auth is needed, set up the handler at browser level then navigate to the
|
|
285
|
+
# initialization page. After navigation succeeds, Chrome has cached the proxy
|
|
286
|
+
# credentials, so we can detach the CDP session.
|
|
287
|
+
if proxy_requires_auth and not did_initial_navigation:
|
|
288
|
+
proxy_cdp_session = (
|
|
289
|
+
await self._setup_proxy_authentication_browser_level(
|
|
290
|
+
browser, config.proxy
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
blank_page = context.pages[0]
|
|
294
|
+
await blank_page.goto(tagged_initialization_url)
|
|
295
|
+
await proxy_cdp_session.detach()
|
|
296
|
+
did_initial_navigation = True
|
|
297
|
+
|
|
298
|
+
# Grab the browser window ID from the page we just opened.
|
|
251
299
|
initialization_page = next(
|
|
252
300
|
(p for p in context.pages if p.url == tagged_initialization_url), None
|
|
253
301
|
)
|
|
@@ -415,6 +463,73 @@ class Narada:
|
|
|
415
463
|
initialization_page
|
|
416
464
|
)
|
|
417
465
|
|
|
466
|
+
async def _setup_proxy_authentication_browser_level(
|
|
467
|
+
self, browser: Browser, proxy_config: ProxyConfig
|
|
468
|
+
) -> CDPSession:
|
|
469
|
+
"""Sets up proxy authentication handling via CDP at the browser level.
|
|
470
|
+
|
|
471
|
+
This uses a browser-level CDP session which can intercept auth challenges before they reach
|
|
472
|
+
individual pages, preventing Chrome from showing the proxy authentication dialog.
|
|
473
|
+
|
|
474
|
+
Chrome caches proxy credentials for the session after the first successful authentication.
|
|
475
|
+
The caller should detach the returned CDP session after the first navigation succeeds.
|
|
476
|
+
"""
|
|
477
|
+
cdp_session = await browser.new_browser_cdp_session()
|
|
478
|
+
|
|
479
|
+
# Enable Fetch domain with a catch-all pattern to intercept auth challenges.
|
|
480
|
+
await cdp_session.send(
|
|
481
|
+
"Fetch.enable",
|
|
482
|
+
{
|
|
483
|
+
"handleAuthRequests": True,
|
|
484
|
+
"patterns": [{"urlPattern": "*"}],
|
|
485
|
+
},
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
async def handle_auth(params: dict[str, Any]) -> None:
|
|
489
|
+
request_id = params.get("requestId")
|
|
490
|
+
auth_challenge = params.get("authChallenge", {})
|
|
491
|
+
|
|
492
|
+
# Only handle proxy auth challenges
|
|
493
|
+
if auth_challenge.get("source") != "Proxy":
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
await cdp_session.send(
|
|
498
|
+
"Fetch.continueWithAuth",
|
|
499
|
+
{
|
|
500
|
+
"requestId": request_id,
|
|
501
|
+
"authChallengeResponse": {
|
|
502
|
+
"response": "ProvideCredentials",
|
|
503
|
+
"username": proxy_config.username,
|
|
504
|
+
"password": proxy_config.password,
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
logging.debug("Browser-level proxy authentication credentials provided")
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logging.error("Failed to respond to proxy auth challenge: %s", e)
|
|
511
|
+
|
|
512
|
+
async def handle_request_paused(params: dict[str, Any]) -> None:
|
|
513
|
+
# Continue all paused requests immediately
|
|
514
|
+
request_id = params.get("requestId")
|
|
515
|
+
try:
|
|
516
|
+
await cdp_session.send(
|
|
517
|
+
"Fetch.continueRequest", {"requestId": request_id}
|
|
518
|
+
)
|
|
519
|
+
except Exception:
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
cdp_session.on(
|
|
523
|
+
"Fetch.authRequired",
|
|
524
|
+
lambda params: asyncio.create_task(handle_auth(params)),
|
|
525
|
+
)
|
|
526
|
+
cdp_session.on(
|
|
527
|
+
"Fetch.requestPaused",
|
|
528
|
+
lambda params: asyncio.create_task(handle_request_paused(params)),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return cdp_session
|
|
532
|
+
|
|
418
533
|
async def _fix_download_behavior(self, side_panel_page: Page) -> None:
|
|
419
534
|
"""Reverts the download behavior to the default behavior for the extension, otherwise our
|
|
420
535
|
extension cannot download files.
|
narada/config.py
CHANGED
|
@@ -22,6 +22,50 @@ def _default_user_data_dir() -> str:
|
|
|
22
22
|
return str(Path("~/.config/narada/user-data-dirs/default").expanduser())
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class ProxyConfig:
|
|
27
|
+
"""Configuration for HTTP/HTTPS/SOCKS5 proxy.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
server: Proxy server URL. HTTP and SOCKS proxies are supported, for example
|
|
31
|
+
"http://myproxy.com:3128" or "socks5://myproxy.com:3128".
|
|
32
|
+
Short form "myproxy.com:3128" is considered an HTTP proxy.
|
|
33
|
+
username: Optional username for proxy authentication.
|
|
34
|
+
password: Optional password for proxy authentication.
|
|
35
|
+
bypass: Optional comma-separated domains to bypass proxy,
|
|
36
|
+
for example ".com, chromium.org, .domain.com".
|
|
37
|
+
ignore_cert_errors: If True, ignore SSL certificate errors. Required for proxies that
|
|
38
|
+
perform HTTPS inspection (MITM). Use with caution.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
server: str
|
|
42
|
+
username: str | None = None
|
|
43
|
+
password: str | None = None
|
|
44
|
+
bypass: str | None = None
|
|
45
|
+
ignore_cert_errors: bool = False
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def requires_authentication(self) -> bool:
|
|
49
|
+
"""Returns True if proxy requires authentication."""
|
|
50
|
+
return self.username is not None and self.password is not None
|
|
51
|
+
|
|
52
|
+
def validate(self) -> None:
|
|
53
|
+
"""Validates the proxy configuration.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If configuration is invalid.
|
|
57
|
+
"""
|
|
58
|
+
if not self.server:
|
|
59
|
+
raise ValueError("Proxy server cannot be empty")
|
|
60
|
+
|
|
61
|
+
# Validate that if one credential is provided, both are provided
|
|
62
|
+
if (self.username is None) != (self.password is None):
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"Both username and password must be provided for proxy authentication, "
|
|
65
|
+
"or neither should be provided"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
25
69
|
@dataclass
|
|
26
70
|
class BrowserConfig:
|
|
27
71
|
executable_path: str = field(default_factory=_default_executable_path)
|
|
@@ -32,6 +76,7 @@ class BrowserConfig:
|
|
|
32
76
|
initialization_url: str = "https://app.narada.ai/initialize"
|
|
33
77
|
extension_id: str = "bhioaidlggjdkheaajakomifblpjmokn"
|
|
34
78
|
interactive: bool = True
|
|
79
|
+
proxy: ProxyConfig | None = None
|
|
35
80
|
|
|
36
81
|
@property
|
|
37
82
|
def cdp_url(self) -> str:
|
narada/window.py
CHANGED
|
@@ -4,12 +4,15 @@ import time
|
|
|
4
4
|
from abc import ABC
|
|
5
5
|
from http import HTTPStatus
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import IO, Any, TypeVar, overload
|
|
7
|
+
from typing import IO, Any, Optional, TypeVar, overload
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
from narada_core.actions.models import (
|
|
11
11
|
AgenticSelectorAction,
|
|
12
12
|
AgenticSelectorRequest,
|
|
13
|
+
AgenticSelectorResponse,
|
|
14
|
+
AgenticMouseActionRequest,
|
|
15
|
+
AgenticMouseAction,
|
|
13
16
|
AgenticSelectors,
|
|
14
17
|
AgentResponse,
|
|
15
18
|
AgentUsage,
|
|
@@ -21,6 +24,7 @@ from narada_core.actions.models import (
|
|
|
21
24
|
ReadGoogleSheetRequest,
|
|
22
25
|
ReadGoogleSheetResponse,
|
|
23
26
|
WriteGoogleSheetRequest,
|
|
27
|
+
RecordedClick,
|
|
24
28
|
)
|
|
25
29
|
from narada_core.errors import (
|
|
26
30
|
NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE,
|
|
@@ -325,14 +329,47 @@ class BaseBrowserWindow(ABC):
|
|
|
325
329
|
fallback_operator_query: str,
|
|
326
330
|
# Larger default timeout because Operator can take a bit to run.
|
|
327
331
|
timeout: int | None = 60,
|
|
328
|
-
) ->
|
|
332
|
+
) -> AgenticSelectorResponse:
|
|
329
333
|
"""Performs an action on an element specified by the given selectors, falling back to using
|
|
330
334
|
the Operator agent if the selectors fail to match a unique element.
|
|
331
335
|
"""
|
|
332
|
-
|
|
336
|
+
response_model = (
|
|
337
|
+
AgenticSelectorResponse
|
|
338
|
+
if action["type"] in {"get_text", "get_property"}
|
|
339
|
+
else None
|
|
340
|
+
)
|
|
341
|
+
result = await self._run_extension_action(
|
|
333
342
|
AgenticSelectorRequest(
|
|
334
343
|
action=action,
|
|
335
344
|
selectors=selectors,
|
|
345
|
+
response_model=response_model,
|
|
346
|
+
fallback_operator_query=fallback_operator_query,
|
|
347
|
+
),
|
|
348
|
+
timeout=timeout,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if result is None:
|
|
352
|
+
return {"value": None}
|
|
353
|
+
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
async def agentic_mouse_action(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
action: AgenticMouseAction,
|
|
360
|
+
recorded_click: RecordedClick,
|
|
361
|
+
fallback_operator_query: str,
|
|
362
|
+
resize_window: bool = True,
|
|
363
|
+
timeout: int | None = 60,
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Performs a mouse action at the specified click coordinates, falling back to using
|
|
366
|
+
the Operator agent if the click fails.
|
|
367
|
+
"""
|
|
368
|
+
return await self._run_extension_action(
|
|
369
|
+
AgenticMouseActionRequest(
|
|
370
|
+
action=action,
|
|
371
|
+
recorded_click=recorded_click,
|
|
372
|
+
resize_window=resize_window,
|
|
336
373
|
fallback_operator_query=fallback_operator_query,
|
|
337
374
|
),
|
|
338
375
|
timeout=timeout,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narada
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.29
|
|
4
4
|
Summary: Python client SDK for Narada
|
|
5
5
|
Project-URL: Homepage, https://github.com/NaradaAI/narada-python-sdk/narada
|
|
6
6
|
Project-URL: Repository, https://github.com/NaradaAI/narada-python-sdk
|
|
@@ -9,7 +9,7 @@ Author-email: Narada <support@narada.ai>
|
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
11
|
Requires-Dist: aiohttp>=3.12.13
|
|
12
|
-
Requires-Dist: narada-core==0.0.
|
|
12
|
+
Requires-Dist: narada-core==0.0.7
|
|
13
13
|
Requires-Dist: playwright>=1.53.0
|
|
14
14
|
Requires-Dist: rich>=14.0.0
|
|
15
15
|
Requires-Dist: semver>=3.0.4
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
narada/__init__.py,sha256=2AnpZVQpkh4-onnvbgHX1Wlq1Fa8ya4vAzy2fbS0bCY,968
|
|
2
|
+
narada/client.py,sha256=_nTRHkiYt7lmUgO0HibVagrPXf523LCbWx8JmuVGgoo,21622
|
|
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=hWkL637Aj7ZoJNFkvg92lSHRBaK72FY2WcCoyxzXKyk,18928
|
|
8
|
+
narada-0.1.29.dist-info/METADATA,sha256=ZRJ6lY2UYwj-kMsCOL5peS1mjni9Kr0Y5ROOfK4uBPE,5146
|
|
9
|
+
narada-0.1.29.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
narada-0.1.29.dist-info/RECORD,,
|
narada-0.1.28.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
narada/__init__.py,sha256=Jk941BV4iy70Ei2MmHkNXEOkZUHBa6Lij_jZZRfiyhg,936
|
|
2
|
-
narada/client.py,sha256=r_aBtqVYEBsr5N9E79qzl8q498F6cnSyejqZWSbB4qU,16768
|
|
3
|
-
narada/config.py,sha256=tvj16P_qAHOFB2O_VlrcizA8SrbmS_Nmkm2r7ltG-VM,1345
|
|
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=OGuEs778xjxHivzWlJIIHKZPqGd4Sn9_YVArt_8TYoU,17762
|
|
8
|
-
narada-0.1.28.dist-info/METADATA,sha256=jVmIONyot5rg8HLvqMIpSIjj2h25ycOoTbQcnb_8TnQ,5146
|
|
9
|
-
narada-0.1.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
-
narada-0.1.28.dist-info/RECORD,,
|
|
File without changes
|