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
28 changes: 28 additions & 0 deletions examples/input_variables_file.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 1 addition & 1 deletion packages/narada/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ 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.
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.41"
version = "0.1.42"
description = "Python client SDK for Narada"
license = "Apache-2.0"
readme = "README.md"
Expand Down
100 changes: 89 additions & 11 deletions packages/narada/src/narada/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, Mapping, TypeGuard, TypeVar, overload, override

import aiohttp
from narada_core.actions.models import (
Expand Down Expand Up @@ -65,6 +66,28 @@
_ResponseModel = TypeVar("_ResponseModel", bound=BaseModel)


class _InputVariableFileReference(BaseModel):
key: str
name: str


type _JsonPrimitive = str | int | float | bool | None
type _InputVariableValue = (
_JsonPrimitive
| IOBase
| list["_InputVariableValue"]
| dict[str, "_InputVariableValue"]
)
type _InputVariables = dict[str, _InputVariableValue]
type _NormalizedInputVariableValue = (
_JsonPrimitive
| _InputVariableFileReference
| list["_NormalizedInputVariableValue"]
| dict[str, "_NormalizedInputVariableValue"]
)
type _NormalizedInputVariables = dict[str, _NormalizedInputVariableValue]


class _PresignedPost(BaseModel):
url: str
fields: dict[str, Any]
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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: Mapping[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,
Expand All @@ -149,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]: ...

Expand All @@ -173,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]: ...

Expand All @@ -196,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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -312,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]]: ...

Expand All @@ -329,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]: ...

Expand All @@ -345,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."""
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

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

Loading