narada 0.2.7__tar.gz → 0.2.9__tar.gz
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-0.2.7 → narada-0.2.9}/PKG-INFO +2 -2
- {narada-0.2.7 → narada-0.2.9}/pyproject.toml +2 -2
- {narada-0.2.7 → narada-0.2.9}/src/narada/agent.py +29 -66
- {narada-0.2.7 → narada-0.2.9}/src/narada/environment.py +2 -63
- narada-0.2.9/tests/test_action_trace.py +80 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_agent.py +41 -1
- {narada-0.2.7 → narada-0.2.9}/.gitignore +0 -0
- {narada-0.2.7 → narada-0.2.9}/README.md +0 -0
- {narada-0.2.7 → narada-0.2.9}/src/narada/__init__.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/src/narada/config.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/src/narada/py.typed +0 -0
- {narada-0.2.7 → narada-0.2.9}/src/narada/utils.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/src/narada/version.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_browser_environment_login.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_client.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_cloud_browser.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_input_variables.py +0 -0
- {narada-0.2.7 → narada-0.2.9}/tests/test_window_human_interaction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narada
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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.1.
|
|
12
|
+
Requires-Dist: narada-core==0.1.6
|
|
13
13
|
Requires-Dist: packaging==24.2
|
|
14
14
|
Requires-Dist: playwright>=1.53.0
|
|
15
15
|
Requires-Dist: rich>=14.0.0
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "narada"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.9"
|
|
4
4
|
description = "Python client SDK for Narada"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
authors = [{ name = "Narada", email = "support@narada.ai" }]
|
|
8
8
|
requires-python = ">=3.12"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"narada-core==0.1.
|
|
10
|
+
"narada-core==0.1.6",
|
|
11
11
|
"aiohttp>=3.12.13",
|
|
12
12
|
"playwright>=1.53.0",
|
|
13
13
|
"rich>=14.0.0",
|
|
@@ -81,8 +81,6 @@ class Agent(Generic[_StructuredOutput]):
|
|
|
81
81
|
self.environment = environment
|
|
82
82
|
self.kind = kind
|
|
83
83
|
|
|
84
|
-
# `reasoning` is only valid with the Core Agent; these two overloads make
|
|
85
|
-
# that constraint type-checkable when callers construct a core-agent instance.
|
|
86
84
|
@overload
|
|
87
85
|
async def run(
|
|
88
86
|
self,
|
|
@@ -160,6 +158,12 @@ class Agent(Generic[_StructuredOutput]):
|
|
|
160
158
|
timeout: int = 1000,
|
|
161
159
|
) -> AgentResponse:
|
|
162
160
|
"""Invokes an agent in the bound Narada environment."""
|
|
161
|
+
if reasoning is not None and not isinstance(self.kind, AgentKind):
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"`reasoning` is only supported for built-in `AgentKind` values; "
|
|
164
|
+
"named Agent Studio agents own reasoning per workflow step"
|
|
165
|
+
)
|
|
166
|
+
|
|
163
167
|
remote_dispatch_response = await self._dispatch_request(
|
|
164
168
|
prompt=prompt,
|
|
165
169
|
clear_chat=clear_chat,
|
|
@@ -246,70 +250,29 @@ class Agent(Generic[_StructuredOutput]):
|
|
|
246
250
|
timeout: int = 1000,
|
|
247
251
|
) -> Response:
|
|
248
252
|
dispatch_agent = self.kind if agent is None else agent
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
on_input_required=on_input_required,
|
|
273
|
-
critic_context=critic_context,
|
|
274
|
-
timeout=timeout,
|
|
275
|
-
)
|
|
276
|
-
else:
|
|
277
|
-
if dispatch_agent is not AgentKind.CORE_AGENT:
|
|
278
|
-
raise ValueError(
|
|
279
|
-
"`reasoning` is only supported with `kind=AgentKind.CORE_AGENT` "
|
|
280
|
-
f"(got kind={dispatch_agent!r})"
|
|
281
|
-
)
|
|
282
|
-
# The CORE_AGENT-specific overloads of `_dispatch_request` split on
|
|
283
|
-
# a narrower `output_schema` discriminator (None vs `type[T]`),
|
|
284
|
-
# which the impl's `type[BaseModel] | None` union doesn't cleanly
|
|
285
|
-
# narrow into without further branching. The public `run()`
|
|
286
|
-
# overloads above already give callers correct return-type
|
|
287
|
-
# narrowing, so the internal forward call bypasses overload
|
|
288
|
-
# disambiguation on this single dimension.
|
|
289
|
-
remote_dispatch_response = await self.environment._dispatch_request( # pyright: ignore[reportCallIssue]
|
|
290
|
-
prompt=prompt,
|
|
291
|
-
agent=dispatch_agent,
|
|
292
|
-
reasoning=reasoning,
|
|
293
|
-
clear_chat=clear_chat,
|
|
294
|
-
generate_gif=generate_gif,
|
|
295
|
-
output_schema=output_schema, # pyright: ignore[reportArgumentType]
|
|
296
|
-
previous_request_id=previous_request_id,
|
|
297
|
-
chat_history=chat_history,
|
|
298
|
-
additional_context=additional_context,
|
|
299
|
-
attachment=attachment,
|
|
300
|
-
time_zone=time_zone,
|
|
301
|
-
user_resource_credentials=user_resource_credentials,
|
|
302
|
-
mcp_servers=mcp_servers,
|
|
303
|
-
secret_variables=secret_variables,
|
|
304
|
-
input_variables=input_variables,
|
|
305
|
-
callback_url=callback_url,
|
|
306
|
-
callback_secret=callback_secret,
|
|
307
|
-
callback_headers=callback_headers,
|
|
308
|
-
on_input_required=on_input_required,
|
|
309
|
-
critic_context=critic_context,
|
|
310
|
-
timeout=timeout,
|
|
311
|
-
)
|
|
312
|
-
return remote_dispatch_response
|
|
253
|
+
return await self.environment._dispatch_request(
|
|
254
|
+
prompt=prompt,
|
|
255
|
+
agent=dispatch_agent,
|
|
256
|
+
reasoning=reasoning,
|
|
257
|
+
clear_chat=clear_chat,
|
|
258
|
+
generate_gif=generate_gif,
|
|
259
|
+
output_schema=output_schema,
|
|
260
|
+
previous_request_id=previous_request_id,
|
|
261
|
+
chat_history=chat_history,
|
|
262
|
+
additional_context=additional_context,
|
|
263
|
+
attachment=attachment,
|
|
264
|
+
time_zone=time_zone,
|
|
265
|
+
user_resource_credentials=user_resource_credentials,
|
|
266
|
+
mcp_servers=mcp_servers,
|
|
267
|
+
secret_variables=secret_variables,
|
|
268
|
+
input_variables=input_variables,
|
|
269
|
+
callback_url=callback_url,
|
|
270
|
+
callback_secret=callback_secret,
|
|
271
|
+
callback_headers=callback_headers,
|
|
272
|
+
on_input_required=on_input_required,
|
|
273
|
+
critic_context=critic_context,
|
|
274
|
+
timeout=timeout,
|
|
275
|
+
)
|
|
313
276
|
|
|
314
277
|
def _browser_environment(self) -> BaseBrowserEnvironment:
|
|
315
278
|
if not isinstance(self.environment, BaseBrowserEnvironment):
|
|
@@ -840,67 +840,13 @@ class Environment(ABC):
|
|
|
840
840
|
"mimeType": mime_type,
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
# `reasoning` is only valid with the Core Agent; these two overloads make
|
|
844
|
-
# that constraint type-checkable. Generic-agent calls fall through to the
|
|
845
|
-
# general overloads below, which do not accept a `reasoning` argument.
|
|
846
|
-
@overload
|
|
847
|
-
async def _dispatch_request(
|
|
848
|
-
self,
|
|
849
|
-
*,
|
|
850
|
-
prompt: str,
|
|
851
|
-
agent: Literal[AgentKind.CORE_AGENT],
|
|
852
|
-
reasoning: ReasoningEffort | None = None,
|
|
853
|
-
clear_chat: bool | None = None,
|
|
854
|
-
generate_gif: bool | None = None,
|
|
855
|
-
output_schema: None = None,
|
|
856
|
-
previous_request_id: str | None = None,
|
|
857
|
-
chat_history: list[RemoteDispatchChatHistoryItem] | None = None,
|
|
858
|
-
additional_context: dict[str, str] | None = None,
|
|
859
|
-
attachment: File | IO[Any] | None = None,
|
|
860
|
-
time_zone: str = "America/Los_Angeles",
|
|
861
|
-
user_resource_credentials: UserResourceCredentials | None = None,
|
|
862
|
-
mcp_servers: list[McpServer] | None = None,
|
|
863
|
-
secret_variables: dict[str, str] | None = None,
|
|
864
|
-
input_variables: Mapping[str, Any] | None = None,
|
|
865
|
-
callback_url: str | None = None,
|
|
866
|
-
callback_secret: str | None = None,
|
|
867
|
-
callback_headers: Mapping[str, Any] | None = None,
|
|
868
|
-
on_input_required: InputRequiredCallback | None = None,
|
|
869
|
-
timeout: int = 1000,
|
|
870
|
-
) -> Response[None]: ...
|
|
871
|
-
|
|
872
|
-
@overload
|
|
873
|
-
async def _dispatch_request(
|
|
874
|
-
self,
|
|
875
|
-
*,
|
|
876
|
-
prompt: str,
|
|
877
|
-
agent: Literal[AgentKind.CORE_AGENT],
|
|
878
|
-
reasoning: ReasoningEffort | None = None,
|
|
879
|
-
clear_chat: bool | None = None,
|
|
880
|
-
generate_gif: bool | None = None,
|
|
881
|
-
output_schema: type[_StructuredOutput],
|
|
882
|
-
previous_request_id: str | None = None,
|
|
883
|
-
chat_history: list[RemoteDispatchChatHistoryItem] | None = None,
|
|
884
|
-
additional_context: dict[str, str] | None = None,
|
|
885
|
-
attachment: File | IO[Any] | None = None,
|
|
886
|
-
time_zone: str = "America/Los_Angeles",
|
|
887
|
-
user_resource_credentials: UserResourceCredentials | None = None,
|
|
888
|
-
mcp_servers: list[McpServer] | None = None,
|
|
889
|
-
secret_variables: dict[str, str] | None = None,
|
|
890
|
-
input_variables: Mapping[str, Any] | None = None,
|
|
891
|
-
callback_url: str | None = None,
|
|
892
|
-
callback_secret: str | None = None,
|
|
893
|
-
callback_headers: Mapping[str, Any] | None = None,
|
|
894
|
-
on_input_required: InputRequiredCallback | None = None,
|
|
895
|
-
timeout: int = 1000,
|
|
896
|
-
) -> Response[_StructuredOutput]: ...
|
|
897
|
-
|
|
898
843
|
@overload
|
|
899
844
|
async def _dispatch_request(
|
|
900
845
|
self,
|
|
901
846
|
*,
|
|
902
847
|
prompt: str,
|
|
903
848
|
agent: AgentKind | str = AgentKind.OPERATOR,
|
|
849
|
+
reasoning: ReasoningEffort | None = None,
|
|
904
850
|
clear_chat: bool | None = None,
|
|
905
851
|
generate_gif: bool | None = None,
|
|
906
852
|
output_schema: None = None,
|
|
@@ -927,6 +873,7 @@ class Environment(ABC):
|
|
|
927
873
|
*,
|
|
928
874
|
prompt: str,
|
|
929
875
|
agent: AgentKind | str = AgentKind.OPERATOR,
|
|
876
|
+
reasoning: ReasoningEffort | None = None,
|
|
930
877
|
clear_chat: bool | None = None,
|
|
931
878
|
generate_gif: bool | None = None,
|
|
932
879
|
output_schema: type[_StructuredOutput],
|
|
@@ -978,14 +925,6 @@ class Environment(ABC):
|
|
|
978
925
|
"""
|
|
979
926
|
await self._ensure_initialized()
|
|
980
927
|
|
|
981
|
-
# The overloads enforce this at type-check time when callers use
|
|
982
|
-
# ``AgentKind.CORE_AGENT``; the runtime check covers string-form agents
|
|
983
|
-
# (``agent="..."``) and callers without a type checker.
|
|
984
|
-
if reasoning is not None and agent is not AgentKind.CORE_AGENT:
|
|
985
|
-
raise ValueError(
|
|
986
|
-
"`reasoning` is only supported with `agent=AgentKind.CORE_AGENT` "
|
|
987
|
-
f"(got agent={agent!r})"
|
|
988
|
-
)
|
|
989
928
|
deadline = time.monotonic() + timeout
|
|
990
929
|
|
|
991
930
|
agent_prefix = (
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from narada_core.tracing.model import OperatorActionTraceItem, parse_action_trace
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_parse_operator_action_trace_exposes_timestamps() -> None:
|
|
9
|
+
trace = parse_action_trace(
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
"url": "https://example.com",
|
|
13
|
+
"action": "Clicked Submit",
|
|
14
|
+
"startTs": "2026-07-20T17:00:00.000Z",
|
|
15
|
+
"endTs": "2026-07-20T17:00:01.500Z",
|
|
16
|
+
"durationMs": 1_500,
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
item = trace[0]
|
|
22
|
+
assert isinstance(item, OperatorActionTraceItem)
|
|
23
|
+
assert item.start_ts == "2026-07-20T17:00:00.000Z"
|
|
24
|
+
assert item.end_ts == "2026-07-20T17:00:01.500Z"
|
|
25
|
+
assert item.duration_ms == 1_500
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
"timestamps",
|
|
30
|
+
[
|
|
31
|
+
{"startTs": "2026-07-20T17:00:00.000Z", "durationMs": 1_000},
|
|
32
|
+
{"endTs": "2026-07-20T17:00:01.000Z", "durationMs": 1_000},
|
|
33
|
+
{
|
|
34
|
+
"startTs": "2026-07-20T17:00:02.000Z",
|
|
35
|
+
"endTs": "2026-07-20T17:00:01.000Z",
|
|
36
|
+
"durationMs": 0,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"startTs": "not-a-timestamp",
|
|
40
|
+
"endTs": "2026-07-20T17:00:01.000Z",
|
|
41
|
+
"durationMs": 1_000,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"startTs": "2026-07-20T17:00:00",
|
|
45
|
+
"endTs": "2026-07-20T17:00:01",
|
|
46
|
+
"durationMs": 1_000,
|
|
47
|
+
},
|
|
48
|
+
{"startTs": 1_000, "endTs": 2_000, "durationMs": 1_000},
|
|
49
|
+
{
|
|
50
|
+
"startTs": "2026-07-20T17:00:00.000Z",
|
|
51
|
+
"endTs": "2026-07-20T17:00:01.000Z",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"startTs": "2026-07-20T17:00:00.000Z",
|
|
55
|
+
"endTs": "2026-07-20T17:00:01.000Z",
|
|
56
|
+
"durationMs": 999,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
)
|
|
60
|
+
def test_operator_action_trace_rejects_invalid_timing(
|
|
61
|
+
timestamps: dict[str, str | int],
|
|
62
|
+
) -> None:
|
|
63
|
+
with pytest.raises(ValidationError):
|
|
64
|
+
OperatorActionTraceItem(
|
|
65
|
+
url="https://example.com",
|
|
66
|
+
action="Clicked Submit",
|
|
67
|
+
**timestamps,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_operator_action_trace_accepts_explicit_utc_offset() -> None:
|
|
72
|
+
item = OperatorActionTraceItem(
|
|
73
|
+
url="https://example.com",
|
|
74
|
+
action="Clicked Submit",
|
|
75
|
+
startTs="2026-07-20T17:00:00.000+00:00",
|
|
76
|
+
endTs="2026-07-20T17:00:01.000+00:00",
|
|
77
|
+
durationMs=1_000,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert item.start_ts.endswith("+00:00")
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
from narada import Agent, Environment
|
|
6
|
+
from narada import Agent, AgentKind, Environment, ReasoningEffort
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class _FakeResponse:
|
|
@@ -100,6 +100,46 @@ async def test_agent_run_reruns_but_environment_initialization_is_cached(
|
|
|
100
100
|
"/Operator first",
|
|
101
101
|
"/Operator second",
|
|
102
102
|
]
|
|
103
|
+
assert all("reasoningMode" not in body for body in fake_session.dispatched_bodies)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
@pytest.mark.parametrize(
|
|
108
|
+
("kind", "reasoning", "expected_prompt"),
|
|
109
|
+
[
|
|
110
|
+
(AgentKind.PRODUCTIVITY, ReasoningEffort.NONE, "analyze"),
|
|
111
|
+
(AgentKind.OPERATOR, ReasoningEffort.LOW, "/Operator analyze"),
|
|
112
|
+
(AgentKind.CORE_AGENT, ReasoningEffort.MEDIUM, "/coreAgent analyze"),
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
async def test_agent_run_forwards_reasoning_for_every_agent_kind(
|
|
116
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
117
|
+
kind: AgentKind | str,
|
|
118
|
+
reasoning: ReasoningEffort,
|
|
119
|
+
expected_prompt: str,
|
|
120
|
+
) -> None:
|
|
121
|
+
import narada.environment as environment_module
|
|
122
|
+
|
|
123
|
+
fake_session = _RemoteDispatchFakeClientSession()
|
|
124
|
+
monkeypatch.setattr(
|
|
125
|
+
environment_module.aiohttp, "ClientSession", lambda: fake_session
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await Agent(environment=_CountingEnvironment(), kind=kind).run(
|
|
129
|
+
"analyze",
|
|
130
|
+
reasoning=reasoning,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert fake_session.dispatched_bodies[0]["prompt"] == expected_prompt
|
|
134
|
+
assert fake_session.dispatched_bodies[0]["reasoningMode"] == reasoning.value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_agent_run_rejects_top_level_reasoning_for_named_agent() -> None:
|
|
139
|
+
agent = Agent(environment=_CountingEnvironment(), kind="/owner/custom-agent")
|
|
140
|
+
|
|
141
|
+
with pytest.raises(ValueError, match="named Agent Studio agents"):
|
|
142
|
+
await agent.run("analyze", reasoning=ReasoningEffort.HIGH)
|
|
103
143
|
|
|
104
144
|
|
|
105
145
|
@pytest.mark.asyncio
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|