diff --git a/predicate/agent_runtime.py b/predicate/agent_runtime.py index a7826b1..06738fa 100644 --- a/predicate/agent_runtime.py +++ b/predicate/agent_runtime.py @@ -125,6 +125,7 @@ def __init__( backend: BrowserBackend, tracer: Tracer, snapshot_options: SnapshotOptions | None = None, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, tool_registry: ToolRegistry | None = None, ): @@ -138,7 +139,8 @@ def __init__( - PlaywrightBackend (future, for direct Playwright) tracer: Tracer for emitting verification events snapshot_options: Default options for snapshots - sentience_api_key: API key for Pro/Enterprise tier (enables Gateway refinement) + predicate_api_key: Canonical API key parameter for Pro/Enterprise tier. + sentience_api_key: Backward-compatible API key alias (legacy name). tool_registry: Optional ToolRegistry for LLM-callable tools """ self.backend = backend @@ -147,8 +149,10 @@ def __init__( # Build default snapshot options with API key if provided default_opts = snapshot_options or SnapshotOptions() - if sentience_api_key: - default_opts.sentience_api_key = sentience_api_key + effective_api_key = predicate_api_key or sentience_api_key + if effective_api_key: + default_opts.predicate_api_key = effective_api_key + default_opts.sentience_api_key = effective_api_key if default_opts.use_api is None: default_opts.use_api = True self._snapshot_options = default_opts @@ -193,6 +197,7 @@ def from_playwright_page( page: Page, tracer: Tracer, snapshot_options: SnapshotOptions | None = None, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, tool_registry: ToolRegistry | None = None, ) -> AgentRuntime: @@ -203,7 +208,8 @@ def from_playwright_page( page: Playwright Page for browser interaction tracer: Tracer for emitting verification events snapshot_options: Default options for snapshots - sentience_api_key: API key for Pro/Enterprise tier + predicate_api_key: Canonical API key parameter for Pro/Enterprise tier. + sentience_api_key: Backward-compatible API key alias (legacy name). tool_registry: Optional ToolRegistry for LLM-callable tools Returns: @@ -216,6 +222,7 @@ def from_playwright_page( backend=backend, tracer=tracer, snapshot_options=snapshot_options, + predicate_api_key=predicate_api_key, sentience_api_key=sentience_api_key, tool_registry=tool_registry, ) @@ -226,6 +233,7 @@ def attach( page: Page, tracer: Tracer, snapshot_options: SnapshotOptions | None = None, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, tool_registry: ToolRegistry | None = None, ) -> AgentRuntime: @@ -236,6 +244,7 @@ def attach( page=page, tracer=tracer, snapshot_options=snapshot_options, + predicate_api_key=predicate_api_key, sentience_api_key=sentience_api_key, tool_registry=tool_registry, ) @@ -247,6 +256,7 @@ async def from_sentience_browser( page: Page, tracer: Tracer, snapshot_options: SnapshotOptions | None = None, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, ) -> AgentRuntime: """ @@ -260,7 +270,8 @@ async def from_sentience_browser( page: Playwright Page for browser interaction tracer: Tracer for emitting verification events snapshot_options: Default options for snapshots - sentience_api_key: API key for Pro/Enterprise tier + predicate_api_key: Canonical API key parameter for Pro/Enterprise tier. + sentience_api_key: Backward-compatible API key alias (legacy name). Returns: AgentRuntime instance @@ -272,6 +283,7 @@ async def from_sentience_browser( backend=backend, tracer=tracer, snapshot_options=snapshot_options, + predicate_api_key=predicate_api_key, sentience_api_key=sentience_api_key, ) # Store browser reference for snapshot() to use diff --git a/predicate/backends/sentience_context.py b/predicate/backends/sentience_context.py index d063531..548c557 100644 --- a/predicate/backends/sentience_context.py +++ b/predicate/backends/sentience_context.py @@ -86,6 +86,7 @@ class SentienceContext: def __init__( self, *, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, use_api: bool | None = None, max_elements: int = 60, @@ -96,13 +97,14 @@ def __init__( Initialize SentienceContext. Args: - sentience_api_key: Sentience API key for gateway mode + predicate_api_key: Canonical API key parameter for gateway mode. + sentience_api_key: Backward-compatible API key alias (legacy name). use_api: Force API vs extension mode (auto-detected if None) max_elements: Maximum elements to fetch from snapshot show_overlay: Show visual overlay highlighting elements in browser top_element_selector: Configuration for element selection strategy """ - self._api_key = sentience_api_key + self._api_key = predicate_api_key or sentience_api_key self._use_api = use_api self._max_elements = max_elements self._show_overlay = show_overlay @@ -155,7 +157,7 @@ async def build( # Set API options if self._api_key: - options.sentience_api_key = self._api_key + options.predicate_api_key = self._api_key if self._use_api is not None: options.use_api = self._use_api elif self._api_key: diff --git a/predicate/backends/snapshot.py b/predicate/backends/snapshot.py index e4dcbf3..9574626 100644 --- a/predicate/backends/snapshot.py +++ b/predicate/backends/snapshot.py @@ -247,11 +247,10 @@ async def snapshot( # Determine if we should use server-side API # Same logic as main snapshot() function in predicate/snapshot.py - should_use_api = ( - options.use_api if options.use_api is not None else (options.sentience_api_key is not None) - ) + effective_api_key = options.predicate_api_key or options.sentience_api_key + should_use_api = options.use_api if options.use_api is not None else (effective_api_key is not None) - if should_use_api and options.sentience_api_key: + if should_use_api and effective_api_key: # Use server-side API (Pro/Enterprise tier) return await _snapshot_via_api(backend, options) else: @@ -596,7 +595,7 @@ async def _snapshot_via_api( try: api_result = await _post_snapshot_to_gateway_async( payload, - options.sentience_api_key, + options.predicate_api_key or options.sentience_api_key, api_url, timeout_s=options.gateway_timeout_s, ) diff --git a/predicate/debugger.py b/predicate/debugger.py index 1b8321c..b3729ac 100644 --- a/predicate/debugger.py +++ b/predicate/debugger.py @@ -48,16 +48,22 @@ def attach( page: Page, tracer: Tracer, snapshot_options: SnapshotOptions | None = None, + predicate_api_key: str | None = None, sentience_api_key: str | None = None, tool_registry: ToolRegistry | None = None, ) -> SentienceDebugger: - runtime = AgentRuntime.from_playwright_page( - page=page, - tracer=tracer, - snapshot_options=snapshot_options, - sentience_api_key=sentience_api_key, - tool_registry=tool_registry, - ) + factory_kwargs: dict[str, Any] = { + "page": page, + "tracer": tracer, + "snapshot_options": snapshot_options, + "sentience_api_key": sentience_api_key, + "tool_registry": tool_registry, + } + # Preserve old call shape unless new parameter is explicitly used. + if predicate_api_key is not None: + factory_kwargs["predicate_api_key"] = predicate_api_key + + runtime = AgentRuntime.from_playwright_page(**factory_kwargs) return cls(runtime=runtime) def begin_step(self, goal: str, step_index: int | None = None) -> str: diff --git a/predicate/models.py b/predicate/models.py index f923b19..2fbf8ea 100644 --- a/predicate/models.py +++ b/predicate/models.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator class BBox(BaseModel): @@ -787,10 +787,24 @@ class SnapshotOptions(BaseModel): ) # API credentials (for browser-use integration without SentienceBrowser) - sentience_api_key: str | None = None # Sentience API key for Pro/Enterprise features + # Keep both names during migration; Predicate name is canonical. + predicate_api_key: str | None = None + sentience_api_key: str | None = None model_config = ConfigDict(arbitrary_types_allowed=True) + @model_validator(mode="after") + def _sync_api_key_aliases(self) -> "SnapshotOptions": + """ + Keep predicate_api_key and sentience_api_key in sync during migration. + Predicate naming wins when both are set. + """ + if self.predicate_api_key: + self.sentience_api_key = self.predicate_api_key + elif self.sentience_api_key: + self.predicate_api_key = self.sentience_api_key + return self + class AgentActionResult(BaseModel): """Result of a single agent action (from agent.act())""" diff --git a/predicate/snapshot.py b/predicate/snapshot.py index 873c7a0..8afdb23 100644 --- a/predicate/snapshot.py +++ b/predicate/snapshot.py @@ -476,9 +476,9 @@ def snapshot( if options is None: options = SnapshotOptions() - # Resolve API key: options.sentience_api_key takes precedence, then browser.api_key - # This allows browser-use users to pass api_key via options without SentienceBrowser - effective_api_key = options.sentience_api_key or browser.api_key + # Resolve API key: predicate_api_key is canonical, sentience_api_key kept for compatibility. + # This allows browser-use users to pass api_key via options without SentienceBrowser. + effective_api_key = options.predicate_api_key or options.sentience_api_key or browser.api_key # Determine if we should use server-side API should_use_api = ( @@ -710,9 +710,9 @@ async def snapshot_async( if options is None: options = SnapshotOptions() - # Resolve API key: options.sentience_api_key takes precedence, then browser.api_key - # This allows browser-use users to pass api_key via options without SentienceBrowser - effective_api_key = options.sentience_api_key or browser.api_key + # Resolve API key: predicate_api_key is canonical, sentience_api_key kept for compatibility. + # This allows browser-use users to pass api_key via options without SentienceBrowser. + effective_api_key = options.predicate_api_key or options.sentience_api_key or browser.api_key # Determine if we should use server-side API should_use_api = ( diff --git a/tests/test_predicate_api_key_aliases.py b/tests/test_predicate_api_key_aliases.py new file mode 100644 index 0000000..41507c5 --- /dev/null +++ b/tests/test_predicate_api_key_aliases.py @@ -0,0 +1,38 @@ +# pylint: disable=protected-access +from unittest.mock import MagicMock + +from predicate.agent_runtime import AgentRuntime +from predicate.models import SnapshotOptions + + +def test_snapshot_options_accepts_predicate_api_key() -> None: + opts = SnapshotOptions(predicate_api_key="pk_test") + assert opts.predicate_api_key == "pk_test" + assert opts.sentience_api_key == "pk_test" + + +def test_snapshot_options_keeps_backward_compatible_sentience_api_key() -> None: + opts = SnapshotOptions(sentience_api_key="sk_test") + assert opts.sentience_api_key == "sk_test" + assert opts.predicate_api_key == "sk_test" + + +def test_agent_runtime_accepts_predicate_api_key() -> None: + runtime = AgentRuntime( + backend=MagicMock(), + tracer=MagicMock(), + predicate_api_key="pk_runtime", + ) + assert runtime._snapshot_options.predicate_api_key == "pk_runtime" + assert runtime._snapshot_options.sentience_api_key == "pk_runtime" + + +def test_agent_runtime_prefers_predicate_api_key_when_both_provided() -> None: + runtime = AgentRuntime( + backend=MagicMock(), + tracer=MagicMock(), + predicate_api_key="pk_new", + sentience_api_key="sk_old", + ) + assert runtime._snapshot_options.predicate_api_key == "pk_new" + assert runtime._snapshot_options.sentience_api_key == "pk_new"