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
2 changes: 1 addition & 1 deletion packages/narada-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "narada-core"
version = "0.0.22"
version = "0.0.23"
description = "Code shared by the `narada` and `narada-pyodide` packages."
license = "Apache-2.0"
readme = "README.md"
Expand Down
61 changes: 61 additions & 0 deletions packages/narada-core/src/narada_core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,66 @@ class ObjectSetPropertiesTrace(TypedDict):
description: str


class PythonStdoutEvent(TypedDict):
kind: Literal["stdout"]
ts: int
text: str


class PythonStderrEvent(TypedDict):
kind: Literal["stderr"]
ts: int
text: str


class PythonSubAgentCallEvent(TypedDict):
kind: Literal["subAgentCall"]
ts_start: int
ts_end: int
agent_type: str
prompt: str
status: Literal["success", "error", "timeout"]
request_id: NotRequired[str]
error_message: NotRequired[str]
action_trace: NotRequired[ActionTrace]


class PythonExtensionActionEvent(TypedDict):
kind: Literal["extensionAction"]
ts_start: int
ts_end: int
action_name: str
request_summary: dict[str, object]
result_summary: NotRequired[dict[str, object]]
status: Literal["success", "error", "timeout"]
error_message: NotRequired[str]


class PythonSideEffectEvent(TypedDict):
kind: Literal["sideEffect"]
ts: int
effect_type: Literal["download_file", "render_html"]
description: str


type PythonTraceEvent = (
PythonStdoutEvent
| PythonStderrEvent
| PythonSubAgentCallEvent
| PythonExtensionActionEvent
| PythonSideEffectEvent
)


class PythonAgentRunTrace(TypedDict):
step_type: Literal["pythonAgentRun"]
url: str
status: Literal["success", "error", "aborted"]
duration_ms: int
events: list[PythonTraceEvent]
error_message: NotRequired[str]


ApaStepTrace = (
GoToUrlTrace
| GetUrlTrace
Expand Down Expand Up @@ -324,6 +384,7 @@ class ObjectSetPropertiesTrace(TypedDict):
| DataTableInsertRowTrace
| DataTableUpdateCellValueTrace
| ObjectSetPropertiesTrace
| PythonAgentRunTrace
)


Expand Down
24 changes: 24 additions & 0 deletions packages/narada-core/src/narada_core/tracing/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@
NonNegativeInt,
TypeAdapter,
ValidationError,
field_validator,
model_validator,
)


def _normalize_agent_type(agent_type: object) -> str:
"""Normalize legacy SDK enum values into the frontend trace contract."""
match agent_type:
case 1 | "1":
return "generalist"
case 2 | "2":
return "operator"
case 3 | "3":
return "coreAgent"
case _:
return str(agent_type)


class OperatorActionTraceItem(BaseModel):
url: str
action: str
Expand Down Expand Up @@ -42,6 +56,11 @@ class AgentTrace(BaseModel):
action_trace: ActionTrace | None = None
text: str | None = None

@field_validator("agent_type", mode="before")
@classmethod
def _normalize_agent_type(cls, value: object) -> str:
return _normalize_agent_type(value)


class ForLoopTrace(BaseModel):
step_type: Literal["for"]
Expand Down Expand Up @@ -244,6 +263,11 @@ class PythonSubAgentCallEvent(BaseModel):
error_message: str | None = None
action_trace: ActionTrace | None = None

@field_validator("agent_type", mode="before")
@classmethod
def _normalize_agent_type(cls, value: object) -> str:
return _normalize_agent_type(value)

@model_validator(mode="after")
def _check_ts_ordering(self) -> PythonSubAgentCallEvent:
if self.ts_end < self.ts_start:
Expand Down
4 changes: 2 additions & 2 deletions packages/narada-pyodide/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@

[project]
name = "narada-pyodide"
version = "0.0.51"
version = "0.0.52"
description = "Pyodide-compatible Python client SDK for Narada"
license = "Apache-2.0"
readme = "README.md"
authors = [{ name = "Narada", email = "support@narada.ai" }]
requires-python = ">=3.12"
dependencies = [
"narada-core==0.0.22",
"narada-core==0.0.23",
# Must be a supported version in https://pyodide.org/en/stable/usage/packages-in-pyodide.html
"packaging==24.2",
]
Expand Down
14 changes: 13 additions & 1 deletion packages/narada-pyodide/src/narada/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ async def _narada_get_id_token() -> str: ...
_ResponseModel = TypeVar("_ResponseModel", bound=BaseModel)


def _trace_agent_type(agent: Agent | str) -> str:
match agent:
case Agent.PRODUCTIVITY:
return "generalist"
case Agent.OPERATOR:
return "operator"
case Agent.CORE_AGENT:
return "coreAgent"
case _:
return str(agent)


def _normalize_narada_env(env: str | None) -> Literal["prod", "dev", None]:
if env is not None and env not in ("prod", "dev"):
raise ValueError(f"Invalid environment: {env!r}")
Expand Down Expand Up @@ -354,7 +366,7 @@ async def dispatch_request(
# exit (successful return, timeout, or non-timeout failure) produces a
# ``subAgentCall`` trace event with matching status. See `_trace.py`.
trace_start_ms = _trace.now_ms()
agent_type_str = agent.value if isinstance(agent, Agent) else str(agent)
agent_type_str = _trace_agent_type(agent)

deadline = time.monotonic() + timeout

Expand Down
39 changes: 38 additions & 1 deletion packages/narada-pyodide/tests/test_cloud_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pytest
from packaging.version import InvalidVersion

PROJECT_ROOT = Path("/Users/zizheng/Projects/narada-python-sdk")
PROJECT_ROOT = Path(__file__).resolve().parents[3]
PYODIDE_SRC = PROJECT_ROOT / "packages" / "narada-pyodide" / "src"
CORE_SRC = PROJECT_ROOT / "packages" / "narada-core" / "src"

Expand Down Expand Up @@ -277,6 +277,43 @@ async def test_cloud_browser_window_dispatch_request_omits_parent_run_ids(
assert "parentRunIds" not in payload


@pytest.mark.asyncio
async def test_dispatch_request_emits_string_trace_agent_type_for_sdk_enum(
monkeypatch: pytest.MonkeyPatch,
) -> None:
pyfetch = AsyncMock(
side_effect=[
_FakeResponse(json_data={"requestId": "req-123"}),
_FakeResponse(json_data={"status": "success", "response": None}),
]
)
narada_pkg, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch)
emitted_events: list[str] = []
monkeypatch.setattr(
sys.modules["narada._trace"],
"_narada_emit_trace_event",
emitted_events.append,
raising=False,
)

window = window_module.CloudBrowserWindow(
browser_window_id="browser-window-123",
session_id="session-123",
api_key="test-api-key",
)
response = await window.dispatch_request(
prompt="hello from cloud browser",
agent=narada_pkg.Agent.OPERATOR,
)

assert response["status"] == "success"
assert json.loads(pyfetch.await_args_list[0].kwargs["body"])["prompt"] == (
"/Operator hello from cloud browser"
)
assert len(emitted_events) == 1
assert json.loads(emitted_events[0])["agent_type"] == "operator"


@pytest.mark.asyncio
async def test_cloud_browser_window_dispatch_request_preserves_current_file_variable_shape(
monkeypatch: pytest.MonkeyPatch,
Expand Down
4 changes: 2 additions & 2 deletions packages/narada/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "narada"
version = "0.1.53a1"
version = "0.1.53a2"
description = "Python client SDK for Narada"
license = "Apache-2.0"
readme = "README.md"
authors = [{ name = "Narada", email = "support@narada.ai" }]
requires-python = ">=3.12"
dependencies = [
"narada-core==0.0.22",
"narada-core==0.0.23",
"aiohttp>=3.12.13",
"playwright>=1.53.0",
"rich>=14.0.0",
Expand Down
6 changes: 3 additions & 3 deletions uv.lock

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

Loading