diff --git a/CLAUDE.md b/CLAUDE.md index 013f13e7..1a8af886 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ client.crawl # Website crawling jobs client.extract # Data extraction jobs client.web # Web fetch/search operations client.web.batch_fetch # Batch web fetch operations -client.agents # AI agent integrations (browser_use, cua, claude_computer_use, hyper_agent, gemini_computer_use) +client.agents # AI agent integrations (browser_use, cua, claude_computer_use, hyper_agent, gemini_computer_use, grok_computer_use, meta_computer_use) client.profiles # Browser profile management client.extensions # Browser extension management client.team # Team/credit info diff --git a/hyperbrowser/client/managers/async_manager/agents/__init__.py b/hyperbrowser/client/managers/async_manager/agents/__init__.py index d73f92ac..601993bd 100644 --- a/hyperbrowser/client/managers/async_manager/agents/__init__.py +++ b/hyperbrowser/client/managers/async_manager/agents/__init__.py @@ -4,6 +4,7 @@ from .hyper_agent import HyperAgentManager from .gemini_computer_use import GeminiComputerUseManager from .grok_computer_use import GrokComputerUseManager +from .meta_computer_use import MetaComputerUseManager class Agents: @@ -14,3 +15,4 @@ def __init__(self, client): self.hyper_agent = HyperAgentManager(client) self.gemini_computer_use = GeminiComputerUseManager(client) self.grok_computer_use = GrokComputerUseManager(client) + self.meta_computer_use = MetaComputerUseManager(client) diff --git a/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py b/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py new file mode 100644 index 00000000..01084dd8 --- /dev/null +++ b/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py @@ -0,0 +1,84 @@ +import asyncio +from typing import Union + +from hyperbrowser.client._request import dump_request +from hyperbrowser.exceptions import HyperbrowserError +from hyperbrowser.types import ( + StartMetaComputerUseTaskParams as StartMetaComputerUseTaskParamsDict, +) + +from .....models import ( + POLLING_ATTEMPTS, + BasicResponse, + MetaComputerUseTaskResponse, + MetaComputerUseTaskStatusResponse, + StartMetaComputerUseTaskParams, + StartMetaComputerUseTaskResponse, +) + + +class MetaComputerUseManager: + def __init__(self, client): + self._client = client + + async def start( + self, + params: Union[ + StartMetaComputerUseTaskParamsDict, + StartMetaComputerUseTaskParams, + ], + ) -> StartMetaComputerUseTaskResponse: + response = await self._client.transport.post( + self._client._build_url("/task/meta-computer-use"), + data=dump_request(params, StartMetaComputerUseTaskParams), + ) + return StartMetaComputerUseTaskResponse(**response.data) + + async def get(self, job_id: str) -> MetaComputerUseTaskResponse: + response = await self._client.transport.get( + self._client._build_url(f"/task/meta-computer-use/{job_id}") + ) + return MetaComputerUseTaskResponse(**response.data) + + async def get_status(self, job_id: str) -> MetaComputerUseTaskStatusResponse: + response = await self._client.transport.get( + self._client._build_url(f"/task/meta-computer-use/{job_id}/status") + ) + return MetaComputerUseTaskStatusResponse(**response.data) + + async def stop(self, job_id: str) -> BasicResponse: + response = await self._client.transport.put( + self._client._build_url(f"/task/meta-computer-use/{job_id}/stop") + ) + return BasicResponse(**response.data) + + async def start_and_wait( + self, + params: Union[ + StartMetaComputerUseTaskParamsDict, + StartMetaComputerUseTaskParams, + ], + ) -> MetaComputerUseTaskResponse: + job_start_resp = await self.start(params) + job_id = job_start_resp.job_id + if not job_id: + raise HyperbrowserError("Failed to start Meta Computer Use task job") + + failures = 0 + while True: + try: + job_response = await self.get_status(job_id) + if ( + job_response.status == "completed" + or job_response.status == "failed" + or job_response.status == "stopped" + ): + return await self.get(job_id) + failures = 0 + except Exception as e: + failures += 1 + if failures >= POLLING_ATTEMPTS: + raise HyperbrowserError( + f"Failed to poll Meta Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}" + ) + await asyncio.sleep(2) diff --git a/hyperbrowser/client/managers/sync_manager/agents/__init__.py b/hyperbrowser/client/managers/sync_manager/agents/__init__.py index d73f92ac..601993bd 100644 --- a/hyperbrowser/client/managers/sync_manager/agents/__init__.py +++ b/hyperbrowser/client/managers/sync_manager/agents/__init__.py @@ -4,6 +4,7 @@ from .hyper_agent import HyperAgentManager from .gemini_computer_use import GeminiComputerUseManager from .grok_computer_use import GrokComputerUseManager +from .meta_computer_use import MetaComputerUseManager class Agents: @@ -14,3 +15,4 @@ def __init__(self, client): self.hyper_agent = HyperAgentManager(client) self.gemini_computer_use = GeminiComputerUseManager(client) self.grok_computer_use = GrokComputerUseManager(client) + self.meta_computer_use = MetaComputerUseManager(client) diff --git a/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py b/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py new file mode 100644 index 00000000..d9996a49 --- /dev/null +++ b/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py @@ -0,0 +1,84 @@ +import time +from typing import Union + +from hyperbrowser.client._request import dump_request +from hyperbrowser.exceptions import HyperbrowserError +from hyperbrowser.types import ( + StartMetaComputerUseTaskParams as StartMetaComputerUseTaskParamsDict, +) + +from .....models import ( + POLLING_ATTEMPTS, + BasicResponse, + MetaComputerUseTaskResponse, + MetaComputerUseTaskStatusResponse, + StartMetaComputerUseTaskParams, + StartMetaComputerUseTaskResponse, +) + + +class MetaComputerUseManager: + def __init__(self, client): + self._client = client + + def start( + self, + params: Union[ + StartMetaComputerUseTaskParamsDict, + StartMetaComputerUseTaskParams, + ], + ) -> StartMetaComputerUseTaskResponse: + response = self._client.transport.post( + self._client._build_url("/task/meta-computer-use"), + data=dump_request(params, StartMetaComputerUseTaskParams), + ) + return StartMetaComputerUseTaskResponse(**response.data) + + def get(self, job_id: str) -> MetaComputerUseTaskResponse: + response = self._client.transport.get( + self._client._build_url(f"/task/meta-computer-use/{job_id}") + ) + return MetaComputerUseTaskResponse(**response.data) + + def get_status(self, job_id: str) -> MetaComputerUseTaskStatusResponse: + response = self._client.transport.get( + self._client._build_url(f"/task/meta-computer-use/{job_id}/status") + ) + return MetaComputerUseTaskStatusResponse(**response.data) + + def stop(self, job_id: str) -> BasicResponse: + response = self._client.transport.put( + self._client._build_url(f"/task/meta-computer-use/{job_id}/stop") + ) + return BasicResponse(**response.data) + + def start_and_wait( + self, + params: Union[ + StartMetaComputerUseTaskParamsDict, + StartMetaComputerUseTaskParams, + ], + ) -> MetaComputerUseTaskResponse: + job_start_resp = self.start(params) + job_id = job_start_resp.job_id + if not job_id: + raise HyperbrowserError("Failed to start Meta Computer Use task job") + + failures = 0 + while True: + try: + job_response = self.get_status(job_id) + if ( + job_response.status == "completed" + or job_response.status == "failed" + or job_response.status == "stopped" + ): + return self.get(job_id) + failures = 0 + except Exception as e: + failures += 1 + if failures >= POLLING_ATTEMPTS: + raise HyperbrowserError( + f"Failed to poll Meta Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}" + ) + time.sleep(2) diff --git a/hyperbrowser/models/__init__.py b/hyperbrowser/models/__init__.py index d51bb155..43d483d5 100644 --- a/hyperbrowser/models/__init__.py +++ b/hyperbrowser/models/__init__.py @@ -127,6 +127,17 @@ StartGrokComputerUseTaskResponse, GrokComputerUseApiKeys, ) +from .agents.meta_computer_use import ( + MetaComputerUseTaskStatus, + MetaComputerUseStepReasoning, + MetaComputerUseStepResponse, + MetaComputerUseTaskData, + MetaComputerUseTaskResponse, + MetaComputerUseTaskStatusResponse, + StartMetaComputerUseTaskParams, + StartMetaComputerUseTaskResponse, + MetaComputerUseApiKeys, +) from .agents.cua import ( CuaTaskData, CuaTaskResponse, @@ -160,6 +171,8 @@ CuaLlm, GrokComputerUseLlm, GrokReasoningEffort, + MetaComputerUseLlm, + MetaReasoningEffort, Country, DownloadsStatus, FetchScreenshotFormat, @@ -496,6 +509,14 @@ "GrokComputerUseTaskStatusResponse", "StartGrokComputerUseTaskParams", "StartGrokComputerUseTaskResponse", + "MetaComputerUseTaskStatus", + "MetaComputerUseStepReasoning", + "MetaComputerUseStepResponse", + "MetaComputerUseTaskData", + "MetaComputerUseTaskResponse", + "MetaComputerUseTaskStatusResponse", + "StartMetaComputerUseTaskParams", + "StartMetaComputerUseTaskResponse", "CuaTaskStatus", "CuaTaskData", "CuaTaskResponse", @@ -506,11 +527,14 @@ "ClaudeComputerUseApiKeys", "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", + "MetaComputerUseApiKeys", "CuaApiKeys", "CuaBaseUrls", "HyperAgentApiKeys", "GrokComputerUseLlm", "GrokReasoningEffort", + "MetaComputerUseLlm", + "MetaReasoningEffort", "HyperAgentOutputV110", "HyperAgentStepV110", # crawl diff --git a/hyperbrowser/models/agents/meta_computer_use.py b/hyperbrowser/models/agents/meta_computer_use.py new file mode 100644 index 00000000..e2da9fc7 --- /dev/null +++ b/hyperbrowser/models/agents/meta_computer_use.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +from typing import Any, List, Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .cua import CuaStepIncompleteDetails, CuaStepResponseError +from ..session import CreateSessionParams +from ..consts import MetaComputerUseLlm, MetaReasoningEffort + +MetaComputerUseTaskStatus = Literal[ + "pending", "running", "completed", "failed", "stopped" +] + + +class MetaComputerUseApiKeys(BaseModel): + """ + API keys for the Meta Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + meta: Optional[str] = Field(default=None, serialization_alias="meta") + + +class StartMetaComputerUseTaskParams(BaseModel): + """ + Parameters for creating a new Meta Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + task: str + llm: Optional[MetaComputerUseLlm] = Field(default=None, serialization_alias="llm") + reasoning_effort: Optional[MetaReasoningEffort] = Field( + default=None, serialization_alias="reasoningEffort" + ) + session_id: Optional[str] = Field(default=None, serialization_alias="sessionId") + max_failures: Optional[int] = Field(default=None, serialization_alias="maxFailures") + max_steps: Optional[int] = Field(default=None, serialization_alias="maxSteps") + keep_browser_open: Optional[bool] = Field( + default=None, serialization_alias="keepBrowserOpen" + ) + session_options: Optional[CreateSessionParams] = Field( + default=None, serialization_alias="sessionOptions" + ) + use_custom_api_keys: Optional[bool] = Field( + default=None, serialization_alias="useCustomApiKeys" + ) + api_keys: Optional[MetaComputerUseApiKeys] = Field( + default=None, serialization_alias="apiKeys" + ) + use_computer_action: Optional[bool] = Field( + default=None, serialization_alias="useComputerAction" + ) + + +class StartMetaComputerUseTaskResponse(BaseModel): + """ + Response from starting a Meta Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + job_id: str = Field(alias="jobId") + live_url: Optional[str] = Field(default=None, alias="liveUrl") + + +class MetaComputerUseTaskStatusResponse(BaseModel): + """ + Response from getting a Meta Computer Use task status. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + status: MetaComputerUseTaskStatus + + +class MetaComputerUseStepReasoning(BaseModel): + """ + Reasoning metadata on a compacted Meta Computer Use step. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + effort: Optional[str] = Field(default=None, serialization_alias="effort") + summary: Optional[str] = Field(default=None, serialization_alias="summary") + + +class MetaComputerUseStepResponse(BaseModel): + """ + Response from a single Meta Computer Use step. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + created_at: Optional[int] = Field(default=None, serialization_alias="created_at") + completed_at: Optional[int] = Field( + default=None, serialization_alias="completed_at" + ) + output_text: Optional[str] = Field(default=None, serialization_alias="output_text") + error: Optional[CuaStepResponseError] = Field( + default=None, serialization_alias="error" + ) + incomplete_details: Optional[CuaStepIncompleteDetails] = Field( + default=None, serialization_alias="incomplete_details" + ) + model: Optional[str] = Field(default=None, serialization_alias="model") + output: Optional[List[Any]] = Field(default=None, serialization_alias="output") + reasoning: Optional[MetaComputerUseStepReasoning] = Field( + default=None, serialization_alias="reasoning" + ) + status: Optional[str] = Field(default=None, serialization_alias="status") + + +class MetaComputerUseTaskData(BaseModel): + model_config = ConfigDict( + populate_by_alias=True, + ) + + steps: List[MetaComputerUseStepResponse] + final_result: Optional[str] = Field(default=None, alias="finalResult") + + +class MetaComputerUseTaskMetadata(BaseModel): + model_config = ConfigDict( + populate_by_alias=True, + ) + + input_tokens: Optional[int] = Field(default=None, alias="inputTokens") + output_tokens: Optional[int] = Field(default=None, alias="outputTokens") + num_task_steps_completed: Optional[int] = Field( + default=None, alias="numTaskStepsCompleted" + ) + + +class MetaComputerUseTaskResponse(BaseModel): + """ + Response from a Meta Computer Use task. + """ + + model_config = ConfigDict( + populate_by_alias=True, + ) + + job_id: str = Field(alias="jobId") + status: MetaComputerUseTaskStatus + metadata: Optional[MetaComputerUseTaskMetadata] = Field( + default=None, alias="metadata" + ) + data: Optional[MetaComputerUseTaskData] = Field(default=None, alias="data") + error: Optional[str] = Field(default=None, alias="error") + live_url: Optional[str] = Field(default=None, alias="liveUrl") diff --git a/hyperbrowser/models/consts.py b/hyperbrowser/models/consts.py index 85269f8e..2335afef 100644 --- a/hyperbrowser/models/consts.py +++ b/hyperbrowser/models/consts.py @@ -106,6 +106,19 @@ "medium", "high", ] +MetaComputerUseLlm = Literal[ + "muse-spark-1.1", + "muse-spark-1.2", + "muse-spark-1.3", +] +MetaReasoningEffort = Literal[ + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] SessionRegion = Literal[ "us", "us-central", diff --git a/hyperbrowser/types/__init__.py b/hyperbrowser/types/__init__.py index 4620519e..f0ebdfc8 100644 --- a/hyperbrowser/types/__init__.py +++ b/hyperbrowser/types/__init__.py @@ -20,12 +20,14 @@ GeminiComputerUseApiKeys, GrokComputerUseApiKeys, HyperAgentApiKeys, + MetaComputerUseApiKeys, StartBrowserUseTaskParams, StartClaudeComputerUseTaskParams, StartCuaTaskParams, StartGeminiComputerUseTaskParams, StartGrokComputerUseTaskParams, StartHyperAgentTaskParams, + StartMetaComputerUseTaskParams, ) from .computer_action import ( ClickActionParams, @@ -184,12 +186,14 @@ "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", "HyperAgentApiKeys", + "MetaComputerUseApiKeys", "StartBrowserUseTaskParams", "StartClaudeComputerUseTaskParams", "StartCuaTaskParams", "StartGeminiComputerUseTaskParams", "StartGrokComputerUseTaskParams", "StartHyperAgentTaskParams", + "StartMetaComputerUseTaskParams", "ClickActionParams", "ComputerActionMouseButton", "ComputerActionParams", diff --git a/hyperbrowser/types/agents.py b/hyperbrowser/types/agents.py index 56fa9a31..63ca110f 100644 --- a/hyperbrowser/types/agents.py +++ b/hyperbrowser/types/agents.py @@ -10,6 +10,8 @@ GeminiComputerUseLlm, GrokComputerUseLlm, GrokReasoningEffort, + MetaComputerUseLlm, + MetaReasoningEffort, HyperAgentLlm, HyperAgentVersion, ) @@ -117,6 +119,28 @@ class StartGrokComputerUseTaskParams(TypedDict, total=False): use_computer_action: Optional[bool] +class MetaComputerUseApiKeys(TypedDict, total=False): + """Provider API keys for a Meta Computer Use task.""" + + meta: Optional[str] + + +class StartMetaComputerUseTaskParams(TypedDict, total=False): + """Parameters for starting a Meta Computer Use task.""" + + task: Required[str] + llm: Optional[MetaComputerUseLlm] + reasoning_effort: Optional[MetaReasoningEffort] + session_id: Optional[str] + max_failures: Optional[int] + max_steps: Optional[int] + keep_browser_open: Optional[bool] + session_options: Optional[CreateSessionParams] + use_custom_api_keys: Optional[bool] + api_keys: Optional[MetaComputerUseApiKeys] + use_computer_action: Optional[bool] + + class CuaApiKeys(TypedDict, total=False): """Provider API keys for an OpenAI CUA task.""" @@ -176,10 +200,12 @@ class StartHyperAgentTaskParams(TypedDict, total=False): "GeminiComputerUseApiKeys", "GrokComputerUseApiKeys", "HyperAgentApiKeys", + "MetaComputerUseApiKeys", "StartBrowserUseTaskParams", "StartClaudeComputerUseTaskParams", "StartCuaTaskParams", "StartGeminiComputerUseTaskParams", "StartGrokComputerUseTaskParams", "StartHyperAgentTaskParams", + "StartMetaComputerUseTaskParams", ] diff --git a/tests/test_meta_computer_use_models.py b/tests/test_meta_computer_use_models.py new file mode 100644 index 00000000..0b9063ce --- /dev/null +++ b/tests/test_meta_computer_use_models.py @@ -0,0 +1,94 @@ +from hyperbrowser.models.agents.meta_computer_use import MetaComputerUseTaskResponse + + +def test_meta_computer_use_task_response_parses_responses_style_step_payload() -> None: + payload = { + "jobId": "job-123", + "status": "completed", + "data": { + "steps": [ + { + "created_at": 1788743504, + "completed_at": 1788743511, + "output_text": ( + "Second sentence in the Ecology section " + '(Population subsection) of the Dog Wikipedia page:\n\n' + '"In 2020, the estimated global dog population was ' + 'between 700 million and 1 billion."' + ), + "error": None, + "incomplete_details": None, + "model": "muse-spark-1.3", + "output": [ + { + "type": "reasoning", + "status": "completed", + "summary": [ + { + "text": "Interpreting the ecology section structure.", + "type": "summary_text", + } + ], + }, + { + "type": "message", + "role": "assistant", + "status": "completed", + "content": [ + { + "type": "output_text", + "text": "Second sentence in the Ecology section.", + } + ], + }, + ], + "reasoning": { + "effort": "medium", + "summary": "concise", + }, + "status": "completed", + }, + { + "created_at": 1788743409, + "completed_at": 1788743410, + "output_text": "", + "error": {"code": "ERR", "message": "something failed"}, + "incomplete_details": {"reason": "timeout"}, + "model": "muse-spark-1.3", + "output": [ + { + "type": "function_call", + "name": "computer.computer", + "arguments": '{"actions":[{"action":"type","text":"google.com\\n"}]}', + "status": "completed", + } + ], + "reasoning": {"effort": "medium", "summary": "concise"}, + "status": "completed", + }, + ], + "finalResult": "result", + }, + } + + result = MetaComputerUseTaskResponse(**payload) + + final_step = result.data.steps[0] + assert final_step.created_at == 1788743504 + assert final_step.completed_at == 1788743511 + assert final_step.error is None + assert final_step.incomplete_details is None + assert final_step.model == "muse-spark-1.3" + assert final_step.reasoning is not None + assert final_step.reasoning.effort == "medium" + assert final_step.reasoning.summary == "concise" + assert final_step.output[0]["type"] == "reasoning" + assert final_step.output[1]["type"] == "message" + + action_step = result.data.steps[1] + assert action_step.error is not None + assert action_step.error.code == "ERR" + assert action_step.error.message == "something failed" + assert action_step.incomplete_details is not None + assert action_step.incomplete_details.reason == "timeout" + assert action_step.output[0]["name"] == "computer.computer" diff --git a/tests/test_request_compat.py b/tests/test_request_compat.py index b052881f..db19c13d 100644 --- a/tests/test_request_compat.py +++ b/tests/test_request_compat.py @@ -11,8 +11,10 @@ CuaBaseUrls, CreateSandboxParams, CreateSessionParams, + MetaComputerUseApiKeys, StartBrowserUseTaskParams, StartCuaTaskParams, + StartMetaComputerUseTaskParams, ) from hyperbrowser.tools import _normalize_extract_tool_params @@ -108,6 +110,38 @@ def test_cua_custom_base_url_serializes_for_mapping_and_legacy_model(): } +def test_meta_computer_use_serializes_for_mapping_and_legacy_model(): + mapping = { + "task": "Complete the task", + "llm": "muse-spark-1.3", + "reasoning_effort": "max", + "use_custom_api_keys": True, + "api_keys": {"meta": "meta-key"}, + "use_computer_action": True, + } + legacy = StartMetaComputerUseTaskParams( + task="Complete the task", + llm="muse-spark-1.3", + reasoning_effort="max", + use_custom_api_keys=True, + api_keys=MetaComputerUseApiKeys(meta="meta-key"), + use_computer_action=True, + ) + + assert dump_request(mapping, StartMetaComputerUseTaskParams) == dump_request( + legacy, + StartMetaComputerUseTaskParams, + ) + assert dump_request(mapping, StartMetaComputerUseTaskParams) == { + "task": "Complete the task", + "llm": "muse-spark-1.3", + "reasoningEffort": "max", + "useCustomApiKeys": True, + "apiKeys": {"meta": "meta-key"}, + "useComputerAction": True, + } + + def test_raw_json_schema_is_copied_without_rewriting_or_dereferencing(): schema = { "$defs": { diff --git a/tests/typecheck/valid_requests.py b/tests/typecheck/valid_requests.py index 40d3323c..7e3587e2 100644 --- a/tests/typecheck/valid_requests.py +++ b/tests/typecheck/valid_requests.py @@ -106,6 +106,15 @@ def valid_sync_requests(client: Hyperbrowser) -> None: }, } ) + client.agents.meta_computer_use.start( + { + "task": "Complete the task", + "llm": "muse-spark-1.3", + "reasoning_effort": "max", + "use_custom_api_keys": True, + "api_keys": {"meta": "meta-key"}, + } + ) client.sandboxes.create( { "image_name": "node", @@ -233,6 +242,13 @@ async def valid_async_requests(client: AsyncHyperbrowser) -> None: }, } ) + await client.agents.meta_computer_use.start( + { + "task": "Complete the task", + "llm": "muse-spark-1.2", + "reasoning_effort": "xhigh", + } + ) await client.sandboxes.create( { "snapshot_name": "ready-to-run",