diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 6e68db6..0c456ef 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -578,6 +578,35 @@ class GetUrlResponse(BaseModel): url: str +class PromptForUserInputVariable(BaseModel): + name: str + type: Literal["string", "number", "boolean", "enum", "dataTable", "object", "array"] + required: bool + enum_values: list[str] | None = None + + +class PromptForUserInputRequest(BaseModel): + name: Literal["prompt_for_user_input"] = "prompt_for_user_input" + step_id: str + variables: list[PromptForUserInputVariable] + + +class PromptForUserInputResponse(BaseModel): + values_by_name: dict[str, Any] + + +class UserApprovalRequest(BaseModel): + name: Literal["user_approval"] = "user_approval" + step_id: str + prompt_message: str + approve_label: str + reject_label: str + + +class UserApprovalResponse(BaseModel): + approved: bool + + type ExtensionActionRequest = ( AgenticSelectorRequest | AgenticMouseActionRequest @@ -590,10 +619,12 @@ class GetUrlResponse(BaseModel): | GetSimplifiedHtmlRequest | GetScreenshotRequest | GetUrlRequest + | PromptForUserInputRequest + | UserApprovalRequest ) class ExtensionActionResponse(BaseModel): - status: Literal["success", "error"] + status: Literal["success", "error", "aborted"] error: str | None = None data: str | None = None diff --git a/packages/narada-core/src/narada_core/errors.py b/packages/narada-core/src/narada_core/errors.py index bf06f13..055b89d 100644 --- a/packages/narada-core/src/narada_core/errors.py +++ b/packages/narada-core/src/narada_core/errors.py @@ -30,3 +30,7 @@ class NaradaExtensionUnauthenticatedError(NaradaError): class NaradaInitializationError(NaradaError): pass + + +class UserAbortedError(Exception): + pass diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index df6bfa1..7a80c4a 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -29,9 +29,14 @@ GetUrlResponse, GoToUrlRequest, PrintMessageRequest, + PromptForUserInputRequest, + PromptForUserInputResponse, + PromptForUserInputVariable, ReadGoogleSheetRequest, ReadGoogleSheetResponse, RecordedClick, + UserApprovalRequest, + UserApprovalResponse, WriteGoogleSheetRequest, parse_action_trace, ) @@ -39,6 +44,7 @@ NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE, NaradaError, NaradaTimeoutError, + UserAbortedError, ) from narada_core.models import ( Agent, @@ -479,6 +485,43 @@ async def print_message(self, *, message: str, timeout: int | None = None) -> No PrintMessageRequest(message=message), timeout=timeout ) + async def prompt_for_user_input( + self, + *, + step_id: str, + variables: list[PromptForUserInputVariable], + timeout: int | None = None, + ) -> dict[str, Any]: + """Prompts the user for one or more input values in the extension UI.""" + result = await self._run_extension_action( + PromptForUserInputRequest(step_id=step_id, variables=variables), + PromptForUserInputResponse, + timeout=timeout, + ) + return result.values_by_name + + async def user_approval( + self, + *, + step_id: str, + prompt_message: str, + approve_label: str, + reject_label: str, + timeout: int | None = None, + ) -> bool: + """Prompts the user to approve or reject in the extension UI.""" + result = await self._run_extension_action( + UserApprovalRequest( + step_id=step_id, + prompt_message=prompt_message, + approve_label=approve_label, + reject_label=reject_label, + ), + UserApprovalResponse, + timeout=timeout, + ) + return result.approved + async def read_google_sheet( self, *, @@ -601,6 +644,8 @@ async def _run_extension_action( response = ExtensionActionResponse.model_validate(resp_json) if response.status == "error": raise NaradaError(response.error) + if response.status == "aborted": + raise UserAbortedError if response_model is None: return None diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 3957d7b..3f6e9fe 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -5,6 +5,7 @@ NaradaInitializationError, NaradaTimeoutError, NaradaUnsupportedBrowserError, + UserAbortedError, ) from narada_core.models import Agent, File, Response, ResponseContent @@ -34,4 +35,5 @@ "render_html", "Response", "ResponseContent", + "UserAbortedError", ] diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 2b61390..29c359b 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -32,9 +32,14 @@ GetUrlResponse, GoToUrlRequest, PrintMessageRequest, + PromptForUserInputRequest, + PromptForUserInputResponse, + PromptForUserInputVariable, ReadGoogleSheetRequest, ReadGoogleSheetResponse, RecordedClick, + UserApprovalRequest, + UserApprovalResponse, WriteGoogleSheetRequest, parse_action_trace, ) @@ -42,6 +47,7 @@ NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE, NaradaError, NaradaTimeoutError, + UserAbortedError, ) from narada_core.models import ( Agent, @@ -540,6 +546,43 @@ async def print_message(self, *, message: str, timeout: int | None = None) -> No PrintMessageRequest(message=message), timeout=timeout ) + async def prompt_for_user_input( + self, + *, + step_id: str, + variables: list[PromptForUserInputVariable], + timeout: int | None = None, + ) -> dict[str, Any]: + """Prompts the user for one or more input values in the extension UI.""" + result = await self._run_extension_action( + PromptForUserInputRequest(step_id=step_id, variables=variables), + PromptForUserInputResponse, + timeout=timeout, + ) + return result.values_by_name + + async def user_approval( + self, + *, + step_id: str, + prompt_message: str, + approve_label: str, + reject_label: str, + timeout: int | None = None, + ) -> bool: + """Prompts the user to approve or reject in the extension UI.""" + result = await self._run_extension_action( + UserApprovalRequest( + step_id=step_id, + prompt_message=prompt_message, + approve_label=approve_label, + reject_label=reject_label, + ), + UserApprovalResponse, + timeout=timeout, + ) + return result.approved + async def read_google_sheet( self, *, @@ -645,6 +688,8 @@ async def _run_extension_action( response = ExtensionActionResponse.model_validate(resp_json) if response.status == "error": raise NaradaError(response.error) + if response.status == "aborted": + raise UserAbortedError if response_model is None: return None