From 5e0f1156b3261a375ed487c677008f45986e2129 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 9 Apr 2026 12:29:38 -0700 Subject: [PATCH 1/5] python sdk attachmet updates --- examples/input_variables_file.py | 28 ++++++++++ packages/narada/README.md | 6 +- packages/narada/pyproject.toml | 2 +- packages/narada/src/narada/window.py | 82 +++++++++++++++++++++++++++- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 examples/input_variables_file.py diff --git a/examples/input_variables_file.py b/examples/input_variables_file.py new file mode 100644 index 0000000..46ee2a0 --- /dev/null +++ b/examples/input_variables_file.py @@ -0,0 +1,28 @@ +import asyncio +from io import BytesIO + +from narada import Agent, Narada + + +async def main() -> None: + async with Narada() as narada: + window = await narada.open_and_initialize_browser_window() + + # Create an in-memory file object and pass it in input_variables. + # The SDK uploads it automatically before dispatching the request. + file_obj = BytesIO( + b"This is a sample document for input_variables file upload." + ) + file_obj.name = "sample_document.txt" + + response = await window.agent( + prompt="Summarize {{$doc}}.", + agent=Agent.CORE_AGENT, + input_variables={"doc": file_obj}, + ) + + print("Response:", response.model_dump_json(indent=2)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/packages/narada/README.md b/packages/narada/README.md index ee6085f..58a84eb 100644 --- a/packages/narada/README.md +++ b/packages/narada/README.md @@ -73,11 +73,15 @@ You can use the SDK to launch browsers and run automated tasks using natural lan ## Migration note -For the next release (`0.1.38`): +For releases `0.1.38` and later: - `variables` has been renamed to `secret_variables`. - Use `input_variables` to pass structured values (objects/arrays) into custom agents. +For releases `0.1.41` and later: + +- For file variables in the Python SDK, pass Python file objects in `input_variables`. + ## Features - **Natural Language Control**: Send instructions in plain English to control browser actions diff --git a/packages/narada/pyproject.toml b/packages/narada/pyproject.toml index 734af9c..594d9c3 100644 --- a/packages/narada/pyproject.toml +++ b/packages/narada/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "narada" -version = "0.1.40" +version = "0.1.41" description = "Python client SDK for Narada" license = "Apache-2.0" readme = "README.md" diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 60a8634..fc7860e 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -5,8 +5,9 @@ from abc import ABC from dataclasses import dataclass from http import HTTPStatus +from io import IOBase from pathlib import Path -from typing import IO, Any, TypeVar, overload, override +from typing import IO, Any, TypeGuard, TypeVar, overload, override import aiohttp from narada_core.actions.models import ( @@ -65,6 +66,28 @@ _ResponseModel = TypeVar("_ResponseModel", bound=BaseModel) +class _InputVariableFileReference(BaseModel): + key: str + name: str + + +_JsonPrimitive = str | int | float | bool | None +_InputVariableValue = ( + _JsonPrimitive + | IOBase + | list["_InputVariableValue"] + | dict[str, "_InputVariableValue"] +) +_InputVariables = dict[str, _InputVariableValue] +_NormalizedInputVariableValue = ( + _JsonPrimitive + | _InputVariableFileReference + | list["_NormalizedInputVariableValue"] + | dict[str, "_NormalizedInputVariableValue"] +) +_NormalizedInputVariables = dict[str, _NormalizedInputVariableValue] + + class _PresignedPost(BaseModel): url: str fields: dict[str, Any] @@ -105,6 +128,11 @@ async def upload_file(self, *, file: IO) -> File: The file is temporarily saved in Narada cloud and expires after 1 day. It can only be accessed by the user who uploaded it. """ + # TODO: We will deprecate this public method in favor of automatic upload via + # input_variables file objects. + return await self._upload_file_impl(file=file) + + async def _upload_file_impl(self, *, file: IO[Any]) -> File: # Get the base filename without directories. filename = Path(file.name).name @@ -132,6 +160,54 @@ async def upload_file(self, *, file: IO) -> File: return File(key=object_key) + async def _normalize_input_variables( + self, *, input_variables: dict[str, Any] + ) -> _NormalizedInputVariables: + normalized: _NormalizedInputVariables = {} + for key, value in input_variables.items(): + normalized[key] = await self._normalize_input_variables_value_impl( + input_variable_value=value + ) + return normalized + + async def _normalize_input_variables_value_impl( + self, *, input_variable_value: Any + ) -> _NormalizedInputVariableValue: + if isinstance(input_variable_value, list): + return [ + await self._normalize_input_variables_value_impl( + input_variable_value=item + ) + for item in input_variable_value + ] + + if self._is_uploadable_file(input_variable_value): + return await self._upload_input_variable_file( + input_variable_value=input_variable_value + ) + + if isinstance(input_variable_value, dict): + normalized: dict[str, _NormalizedInputVariableValue] = {} + for key, value in input_variable_value.items(): + normalized[key] = await self._normalize_input_variables_value_impl( + input_variable_value=value + ) + return normalized + + return input_variable_value + + @staticmethod + def _is_uploadable_file(value: Any) -> TypeGuard[IO[Any]]: + # Keep runtime eligibility aligned with current upload_file expectations. + return isinstance(value, IOBase) and hasattr(value, "name") + + async def _upload_input_variable_file( + self, *, input_variable_value: IO[Any] + ) -> _InputVariableFileReference: + filename = Path(input_variable_value.name).name + uploaded_file = await self._upload_file_impl(file=input_variable_value) + return _InputVariableFileReference(key=uploaded_file["key"], name=filename) + @overload async def dispatch_request( self, @@ -242,7 +318,9 @@ async def dispatch_request( if secret_variables is not None: body["secretVariables"] = secret_variables if input_variables is not None: - body["inputVariables"] = input_variables + body["inputVariables"] = await self._normalize_input_variables( + input_variables=input_variables + ) if callback_url is not None: body["callbackUrl"] = callback_url if callback_secret is not None: From 66fd0bcfca71868065dbd06394935e4fe56e98fd Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 9 Apr 2026 12:49:16 -0700 Subject: [PATCH 2/5] deleted doc in readme --- packages/narada/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/narada/README.md b/packages/narada/README.md index 58a84eb..b19fda4 100644 --- a/packages/narada/README.md +++ b/packages/narada/README.md @@ -78,10 +78,6 @@ For releases `0.1.38` and later: - `variables` has been renamed to `secret_variables`. - Use `input_variables` to pass structured values (objects/arrays) into custom agents. -For releases `0.1.41` and later: - -- For file variables in the Python SDK, pass Python file objects in `input_variables`. - ## Features - **Natural Language Control**: Send instructions in plain English to control browser actions From 54966e3007514581aca8042d7322b9ce1d409704 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 9 Apr 2026 12:49:50 -0700 Subject: [PATCH 3/5] didnt merge main --- packages/narada/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/narada/pyproject.toml b/packages/narada/pyproject.toml index 594d9c3..cab7376 100644 --- a/packages/narada/pyproject.toml +++ b/packages/narada/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "narada" -version = "0.1.41" +version = "0.1.42" description = "Python client SDK for Narada" license = "Apache-2.0" readme = "README.md" From 4ae352855c911dfb41a6c6cd485c430c31ef047c Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 9 Apr 2026 12:56:01 -0700 Subject: [PATCH 4/5] update uv lock --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 80c860f..5fd8861 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -312,7 +312,7 @@ wheels = [ [[package]] name = "narada" -version = "0.1.41" +version = "0.1.42" source = { editable = "packages/narada" } dependencies = [ { name = "aiohttp" }, From b2ee79ecb52cd565a414890ab28bd16c8235c1ce Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 9 Apr 2026 13:02:41 -0700 Subject: [PATCH 5/5] zizheng comments --- packages/narada/src/narada/window.py | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index fc7860e..2b61390 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -7,7 +7,7 @@ from http import HTTPStatus from io import IOBase from pathlib import Path -from typing import IO, Any, TypeGuard, TypeVar, overload, override +from typing import IO, Any, Mapping, TypeGuard, TypeVar, overload, override import aiohttp from narada_core.actions.models import ( @@ -71,21 +71,21 @@ class _InputVariableFileReference(BaseModel): name: str -_JsonPrimitive = str | int | float | bool | None -_InputVariableValue = ( +type _JsonPrimitive = str | int | float | bool | None +type _InputVariableValue = ( _JsonPrimitive | IOBase | list["_InputVariableValue"] | dict[str, "_InputVariableValue"] ) -_InputVariables = dict[str, _InputVariableValue] -_NormalizedInputVariableValue = ( +type _InputVariables = dict[str, _InputVariableValue] +type _NormalizedInputVariableValue = ( _JsonPrimitive | _InputVariableFileReference | list["_NormalizedInputVariableValue"] | dict[str, "_NormalizedInputVariableValue"] ) -_NormalizedInputVariables = dict[str, _NormalizedInputVariableValue] +type _NormalizedInputVariables = dict[str, _NormalizedInputVariableValue] class _PresignedPost(BaseModel): @@ -161,7 +161,7 @@ async def _upload_file_impl(self, *, file: IO[Any]) -> File: return File(key=object_key) async def _normalize_input_variables( - self, *, input_variables: dict[str, Any] + self, *, input_variables: Mapping[str, Any] ) -> _NormalizedInputVariables: normalized: _NormalizedInputVariables = {} for key, value in input_variables.items(): @@ -225,10 +225,10 @@ async def dispatch_request( user_resource_credentials: UserResourceCredentials | None = None, mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, callback_url: str | None = None, callback_secret: str | None = None, - callback_headers: dict[str, Any] | None = None, + callback_headers: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> Response[None]: ... @@ -249,10 +249,10 @@ async def dispatch_request( user_resource_credentials: UserResourceCredentials | None = None, mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, callback_url: str | None = None, callback_secret: str | None = None, - callback_headers: dict[str, Any] | None = None, + callback_headers: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> Response[_StructuredOutput]: ... @@ -272,10 +272,10 @@ async def dispatch_request( user_resource_credentials: UserResourceCredentials | None = None, mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, callback_url: str | None = None, callback_secret: str | None = None, - callback_headers: dict[str, Any] | None = None, + callback_headers: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> Response: """Low-level API for invoking an agent in the Narada extension side panel chat. @@ -390,7 +390,7 @@ async def agent( time_zone: str = "America/Los_Angeles", mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> AgentResponse[dict[str, Any]]: ... @@ -407,7 +407,7 @@ async def agent( time_zone: str = "America/Los_Angeles", mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> AgentResponse[_StructuredOutput]: ... @@ -423,7 +423,7 @@ async def agent( time_zone: str = "America/Los_Angeles", mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, - input_variables: dict[str, Any] | None = None, + input_variables: Mapping[str, Any] | None = None, timeout: int = 1000, ) -> AgentResponse: """Invokes an agent in the Narada extension side panel chat."""