diff --git a/packages/narada-core/pyproject.toml b/packages/narada-core/pyproject.toml index ca75dc0..edadabf 100644 --- a/packages/narada-core/pyproject.toml +++ b/packages/narada-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "narada-core" -version = "0.0.20" +version = "0.0.21" description = "Code shared by the `narada` and `narada-pyodide` packages." license = "Apache-2.0" readme = "README.md" diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 19d23ca..d7d075f 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -21,6 +21,18 @@ def prompt_prefix(self) -> str: return "/coreAgent " +class ReasoningEffort(StrEnum): + """Controls how much reasoning the Core Agent uses before responding. + + Only `Agent.CORE_AGENT` supports this option; other agents raise `ValueError`. + """ + + NONE = "none" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + + class UserResourceCredentials(TypedDict, total=False): salesforce: dict[str, str] jira: dict[str, str] diff --git a/packages/narada-pyodide/pyproject.toml b/packages/narada-pyodide/pyproject.toml index 1443d3e..15bcbdd 100644 --- a/packages/narada-pyodide/pyproject.toml +++ b/packages/narada-pyodide/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "narada-pyodide" -version = "0.0.47" +version = "0.0.48" 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.20", + "narada-core==0.0.21", # Must be a supported version in https://pyodide.org/en/stable/usage/packages-in-pyodide.html "packaging==24.2", ] diff --git a/packages/narada-pyodide/src/narada/__init__.py b/packages/narada-pyodide/src/narada/__init__.py index 544d452..386ed83 100644 --- a/packages/narada-pyodide/src/narada/__init__.py +++ b/packages/narada-pyodide/src/narada/__init__.py @@ -2,7 +2,7 @@ NaradaError, NaradaTimeoutError, ) -from narada_core.models import Agent, File, Response, ResponseContent +from narada_core.models import Agent, File, ReasoningEffort, Response, ResponseContent from narada.client import Narada from narada.utils import download_file, render_html @@ -23,6 +23,7 @@ "Narada", "NaradaError", "NaradaTimeoutError", + "ReasoningEffort", "RemoteBrowserWindow", "render_html", "Response", diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 8f6c57f..2a74d42 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -65,6 +65,7 @@ Agent, File, McpServer, + ReasoningEffort, RemoteDispatchChatHistoryItem, Response, UserResourceCredentials, @@ -199,6 +200,57 @@ async def upload_file(self, *, file: IO) -> File: "Uploading files is not supported in the browser environment" ) + # `reasoning` is only valid with the Core Agent; these two overloads make + # that constraint type-checkable. Generic-agent calls fall through to the + # general overloads below, which do not accept a `reasoning` argument. + @overload + async def dispatch_request( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: None = None, + previous_request_id: str | None = None, + chat_history: list[RemoteDispatchChatHistoryItem] | None = None, + additional_context: dict[str, str] | None = None, + time_zone: str = "America/Los_Angeles", + 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, + callback_url: str | None = None, + callback_secret: str | None = None, + callback_headers: dict[str, Any] | None = None, + timeout: int = 1000, + ) -> Response[None]: ... + + @overload + async def dispatch_request( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: type[_StructuredOutput], + previous_request_id: str | None = None, + chat_history: list[RemoteDispatchChatHistoryItem] | None = None, + additional_context: dict[str, str] | None = None, + time_zone: str = "America/Los_Angeles", + 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, + callback_url: str | None = None, + callback_secret: str | None = None, + callback_headers: dict[str, Any] | None = None, + timeout: int = 1000, + ) -> Response[_StructuredOutput]: ... + @overload async def dispatch_request( self, @@ -250,6 +302,7 @@ async def dispatch_request( *, prompt: str, agent: Agent | str = Agent.OPERATOR, + reasoning: ReasoningEffort | None = None, clear_chat: bool | None = None, generate_gif: bool | None = None, output_schema: type[BaseModel] | None = None, @@ -270,6 +323,14 @@ async def dispatch_request( The higher-level `agent` method should be preferred for most use cases. """ + # The overloads enforce this at type-check time when callers use + # ``Agent.CORE_AGENT``; the runtime check covers string-form agents + # (``agent="..."``) and callers without a type checker. + if reasoning is not None and agent is not Agent.CORE_AGENT: + raise ValueError( + "`reasoning` is only supported with `agent=Agent.CORE_AGENT` " + f"(got agent={agent!r})" + ) # Trace instrumentation: the entire method body is wrapped so that any # exit (successful return, timeout, or non-timeout failure) produces a # ``subAgentCall`` trace event with matching status. See `_trace.py`. @@ -322,6 +383,8 @@ async def dispatch_request( body["callbackSecret"] = callback_secret if callback_headers is not None: body["callbackHeaders"] = callback_headers + if reasoning is not None: + body["reasoningMode"] = reasoning.value try: controller = AbortController.new() @@ -439,6 +502,42 @@ async def dispatch_request( ) raise + # `reasoning` is only valid with the Core Agent. See `dispatch_request` + # above for the rationale; the same overload pattern is mirrored here. + @overload + async def agent( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: None = None, + 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, + timeout: int = 1000, + ) -> AgentResponse[dict[str, Any]]: ... + + @overload + async def agent( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: type[_StructuredOutput], + 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, + timeout: int = 1000, + ) -> AgentResponse[_StructuredOutput]: ... + @overload async def agent( self, @@ -476,6 +575,7 @@ async def agent( *, prompt: str, agent: Agent | str = Agent.OPERATOR, + reasoning: ReasoningEffort | None = None, clear_chat: bool | None = None, generate_gif: bool | None = None, output_schema: type[BaseModel] | None = None, @@ -486,18 +586,49 @@ async def agent( timeout: int = 1000, ) -> AgentResponse: """Invokes an agent in the Narada extension side panel chat.""" - remote_dispatch_response = await self.dispatch_request( - prompt=prompt, - agent=agent, - clear_chat=clear_chat, - generate_gif=generate_gif, - output_schema=output_schema, - time_zone=time_zone, - mcp_servers=mcp_servers, - secret_variables=secret_variables, - input_variables=input_variables, - timeout=timeout, - ) + # Branch on `reasoning` so each call site binds a single, typed overload + # of `dispatch_request`. The validation also lives in `dispatch_request` + # itself (defense in depth + reachable when callers go straight to the + # low-level API), so the redundancy here is intentional. + if reasoning is None: + remote_dispatch_response = await self.dispatch_request( + prompt=prompt, + agent=agent, + clear_chat=clear_chat, + generate_gif=generate_gif, + output_schema=output_schema, + time_zone=time_zone, + mcp_servers=mcp_servers, + secret_variables=secret_variables, + input_variables=input_variables, + timeout=timeout, + ) + else: + if agent is not Agent.CORE_AGENT: + raise ValueError( + "`reasoning` is only supported with `agent=Agent.CORE_AGENT` " + f"(got agent={agent!r})" + ) + # The CORE_AGENT-specific overloads of `dispatch_request` split on + # a narrower `output_schema` discriminator (None vs `type[T]`), + # which the impl's `type[BaseModel] | None` union doesn't cleanly + # narrow into without further branching. The public `agent()` + # overloads above already give callers correct return-type + # narrowing, so the internal forward call bypasses overload + # disambiguation on this single dimension. + remote_dispatch_response = await self.dispatch_request( # pyright: ignore[reportCallIssue] + prompt=prompt, + agent=agent, + reasoning=reasoning, + clear_chat=clear_chat, + generate_gif=generate_gif, + output_schema=output_schema, # pyright: ignore[reportArgumentType] + time_zone=time_zone, + mcp_servers=mcp_servers, + secret_variables=secret_variables, + input_variables=input_variables, + timeout=timeout, + ) response_content = remote_dispatch_response["response"] assert response_content is not None diff --git a/packages/narada/pyproject.toml b/packages/narada/pyproject.toml index f5be009..6575246 100644 --- a/packages/narada/pyproject.toml +++ b/packages/narada/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "narada" -version = "0.1.47" +version = "0.1.48" 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.20", + "narada-core==0.0.21", "aiohttp>=3.12.13", "playwright>=1.53.0", "rich>=14.0.0", diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 3f6e9fe..1434183 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -7,7 +7,7 @@ NaradaUnsupportedBrowserError, UserAbortedError, ) -from narada_core.models import Agent, File, Response, ResponseContent +from narada_core.models import Agent, File, ReasoningEffort, Response, ResponseContent from narada.client import Narada from narada.config import BrowserConfig, ProxyConfig @@ -31,6 +31,7 @@ "NaradaTimeoutError", "NaradaUnsupportedBrowserError", "ProxyConfig", + "ReasoningEffort", "RemoteBrowserWindow", "render_html", "Response", diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index f47debc..dff0332 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -66,6 +66,7 @@ Agent, File, McpServer, + ReasoningEffort, RemoteDispatchChatHistoryItem, Response, UserResourceCredentials, @@ -236,6 +237,59 @@ async def _upload_input_variable_file( "mimeType": mime_type, } + # `reasoning` is only valid with the Core Agent; these two overloads make + # that constraint type-checkable. Generic-agent calls fall through to the + # general overloads below, which do not accept a `reasoning` argument. + @overload + async def dispatch_request( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: None = None, + previous_request_id: str | None = None, + chat_history: list[RemoteDispatchChatHistoryItem] | None = None, + additional_context: dict[str, str] | None = None, + attachment: File | None = None, + time_zone: str = "America/Los_Angeles", + user_resource_credentials: UserResourceCredentials | None = None, + mcp_servers: list[McpServer] | None = None, + secret_variables: dict[str, str] | None = None, + input_variables: Mapping[str, Any] | None = None, + callback_url: str | None = None, + callback_secret: str | None = None, + callback_headers: Mapping[str, Any] | None = None, + timeout: int = 1000, + ) -> Response[None]: ... + + @overload + async def dispatch_request( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: type[_StructuredOutput], + previous_request_id: str | None = None, + chat_history: list[RemoteDispatchChatHistoryItem] | None = None, + additional_context: dict[str, str] | None = None, + attachment: File | None = None, + time_zone: str = "America/Los_Angeles", + user_resource_credentials: UserResourceCredentials | None = None, + mcp_servers: list[McpServer] | None = None, + secret_variables: dict[str, str] | None = None, + input_variables: Mapping[str, Any] | None = None, + callback_url: str | None = None, + callback_secret: str | None = None, + callback_headers: Mapping[str, Any] | None = None, + timeout: int = 1000, + ) -> Response[_StructuredOutput]: ... + @overload async def dispatch_request( self, @@ -289,6 +343,7 @@ async def dispatch_request( *, prompt: str, agent: Agent | str = Agent.OPERATOR, + reasoning: ReasoningEffort | None = None, clear_chat: bool | None = None, generate_gif: bool | None = None, output_schema: type[BaseModel] | None = None, @@ -310,6 +365,14 @@ async def dispatch_request( The higher-level `agent` method should be preferred for most use cases. """ + # The overloads enforce this at type-check time when callers use + # ``Agent.CORE_AGENT``; the runtime check covers string-form agents + # (``agent="..."``) and callers without a type checker. + if reasoning is not None and agent is not Agent.CORE_AGENT: + raise ValueError( + "`reasoning` is only supported with `agent=Agent.CORE_AGENT` " + f"(got agent={agent!r})" + ) deadline = time.monotonic() + timeout agent_prefix = ( @@ -355,6 +418,8 @@ async def dispatch_request( body["callbackSecret"] = callback_secret if callback_headers is not None: body["callbackHeaders"] = callback_headers + if reasoning is not None: + body["reasoningMode"] = reasoning.value try: async with aiohttp.ClientSession() as session: @@ -405,6 +470,44 @@ async def dispatch_request( except asyncio.TimeoutError: raise NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE(timeout) + # `reasoning` is only valid with the Core Agent. See `dispatch_request` + # above for the rationale; the same overload pattern is mirrored here. + @overload + async def agent( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: None = None, + attachment: File | None = None, + time_zone: str = "America/Los_Angeles", + mcp_servers: list[McpServer] | None = None, + secret_variables: dict[str, str] | None = None, + input_variables: Mapping[str, Any] | None = None, + timeout: int = 1000, + ) -> AgentResponse[dict[str, Any]]: ... + + @overload + async def agent( + self, + *, + prompt: str, + agent: Literal[Agent.CORE_AGENT], + reasoning: ReasoningEffort | None = None, + clear_chat: bool | None = None, + generate_gif: bool | None = None, + output_schema: type[_StructuredOutput], + attachment: File | None = None, + time_zone: str = "America/Los_Angeles", + mcp_servers: list[McpServer] | None = None, + secret_variables: dict[str, str] | None = None, + input_variables: Mapping[str, Any] | None = None, + timeout: int = 1000, + ) -> AgentResponse[_StructuredOutput]: ... + @overload async def agent( self, @@ -444,6 +547,7 @@ async def agent( *, prompt: str, agent: Agent | str = Agent.OPERATOR, + reasoning: ReasoningEffort | None = None, clear_chat: bool | None = None, generate_gif: bool | None = None, output_schema: type[BaseModel] | None = None, @@ -455,19 +559,51 @@ async def agent( timeout: int = 1000, ) -> AgentResponse: """Invokes an agent in the Narada extension side panel chat.""" - remote_dispatch_response = await self.dispatch_request( - prompt=prompt, - agent=agent, - clear_chat=clear_chat, - generate_gif=generate_gif, - output_schema=output_schema, - attachment=attachment, - time_zone=time_zone, - mcp_servers=mcp_servers, - secret_variables=secret_variables, - input_variables=input_variables, - timeout=timeout, - ) + # Branch on `reasoning` so each call site binds a single, typed overload + # of `dispatch_request`. The validation also lives in `dispatch_request` + # itself (defense in depth + reachable when callers go straight to the + # low-level API), so the redundancy here is intentional. + if reasoning is None: + remote_dispatch_response = await self.dispatch_request( + prompt=prompt, + agent=agent, + clear_chat=clear_chat, + generate_gif=generate_gif, + output_schema=output_schema, + attachment=attachment, + time_zone=time_zone, + mcp_servers=mcp_servers, + secret_variables=secret_variables, + input_variables=input_variables, + timeout=timeout, + ) + else: + if agent is not Agent.CORE_AGENT: + raise ValueError( + "`reasoning` is only supported with `agent=Agent.CORE_AGENT` " + f"(got agent={agent!r})" + ) + # The CORE_AGENT-specific overloads of `dispatch_request` split on + # a narrower `output_schema` discriminator (None vs `type[T]`), + # which the impl's `type[BaseModel] | None` union doesn't cleanly + # narrow into without further branching. The public `agent()` + # overloads above already give callers correct return-type + # narrowing, so the internal forward call bypasses overload + # disambiguation on this single dimension. + remote_dispatch_response = await self.dispatch_request( # pyright: ignore[reportCallIssue] + prompt=prompt, + agent=agent, + reasoning=reasoning, + clear_chat=clear_chat, + generate_gif=generate_gif, + output_schema=output_schema, # pyright: ignore[reportArgumentType] + attachment=attachment, + time_zone=time_zone, + mcp_servers=mcp_servers, + secret_variables=secret_variables, + input_variables=input_variables, + timeout=timeout, + ) response_content = remote_dispatch_response["response"] assert response_content is not None diff --git a/uv.lock b/uv.lock index dc19511..2c454fd 100644 --- a/uv.lock +++ b/uv.lock @@ -312,7 +312,7 @@ wheels = [ [[package]] name = "narada" -version = "0.1.47" +version = "0.1.48" source = { editable = "packages/narada" } dependencies = [ { name = "aiohttp" }, @@ -345,7 +345,7 @@ dev = [ [[package]] name = "narada-core" -version = "0.0.20" +version = "0.0.21" source = { editable = "packages/narada-core" } dependencies = [ { name = "pydantic" }, @@ -356,7 +356,7 @@ requires-dist = [{ name = "pydantic", specifier = "==2.12.5" }] [[package]] name = "narada-pyodide" -version = "0.0.47" +version = "0.0.48" source = { editable = "packages/narada-pyodide" } dependencies = [ { name = "narada-core" },