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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,5 @@ output/*

.yaml

murphy/output/test_plan.yaml
browser_profile/
7 changes: 3 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,9 @@ We use a thin wrapper around CDP called cdp-use: https://github.com/browser-use/
`cdp_client.send.Target.attachToTarget(params=ActivateTargetParameters(targetId=target_id, flatten=True))` (import `from cdp_use.cdp.target import ActivateTargetParameters`)
- `cdp_client.register.Browser.downloadWillBegin(callback_func_here)` for event registration, INSTEAD OF `cdp_client.on(...)` which does not exist!

## Keep Examples & Tests Up-To-Date
## Keep Tests Up-To-Date

- Make sure to read relevant examples in the `examples/` directory for context and keep them up-to-date when making changes.
- Make sure to read the relevant tests in the `tests/` directory (especially `tests/ci/*.py`) and keep them up-to-date as well.
- Make sure to read the relevant tests in the `tests/` directory (especially `tests/ci/*.py`) and keep them up-to-date as well.
- Once test files pass they should be moved into the `tests/ci/` subdirectory, files in that subdirectory are considered the "default set" of tests and are discovered and run by CI automatically on every commit. Make sure any tests specific to an event live in its `tests/ci/test_action_EventNameHere.py` file.
- Never mock anything in tests, always use real objects!! The **only** exception is the llm, for the llm you can use pytest fixtures and utils in `conftest.py` to set up LLM responses. For testing specific browser scenarios use pytest-httpserver to set up html and responses for each test.
- Never use real remote URLs in tests (e.g. `https://google.com` or `https://example.com`), instead use pytest-httpserver to set up a test server in a fixture that responds with the html needed for the test (see other `tests/ci` files for examples)
Expand Down Expand Up @@ -141,7 +140,7 @@ When making any significant changes:
3. Then implement the changes for the new design. Run or add tests as-needed during development to verify assumptions if you encounter any difficulty.
4. Run the full `tests/ci` suite once the changes are done. Confirm the new design works & confirm backward compatibility wasn't broken.
5. Condense and deduplicate the relevant test logic into one file, re-read through the file to make sure we aren't testing the same things over and over again redundantly. Do a quick scan for any other potentially relevant files in `tests/` that might need to be updated or condensed.
6. Update any relevant files in `docs/` and `examples/` and confirm they match the implementation and tests
6. Update any relevant files in `docs/` and confirm they match the implementation and tests

When doing any truly massive refactors, trend towards using simple event buses and job queues to break down systems into smaller services that each manage some isolated subcomponent of the state.

Expand Down
44 changes: 44 additions & 0 deletions Dockerfile.murphy-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Murphy API server — lightweight Dockerfile for REST API deployment.
#
# Usage:
# docker build -f Dockerfile.murphy-api -t murphy-api .
# docker run -d -p 8000:8000 --env-file .env murphy-api
#
# Required env vars: MURPHY_API_KEY, OPENAI_API_KEY
# Optional env vars: MURPHY_MAX_CONCURRENT_JOBS (default: 2), MURPHY_API_PORT (default: 8000)

FROM python:3.11-slim

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
UV_CACHE_DIR=/root/.cache/uv \
UV_LINK_MODE=copy \
CHROME_PATH=/usr/bin/chromium

# Install Chromium + minimal deps
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium \
fonts-liberation \
fonts-dejavu-core \
libnss3 \
libxss1 \
libasound2 \
libatk-bridge2.0-0 \
libgtk-3-0 \
&& rm -rf /var/lib/apt/lists/*

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Install dependencies first (layer caching)
COPY pyproject.toml uv.lock* /app/
RUN uv venv --python 3.11 && uv sync --no-dev --no-install-project

# Copy source
COPY . /app
RUN uv sync --no-dev

EXPOSE 8000

CMD ["uv", "run", "uvicorn", "murphy.api:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "1800"]
11 changes: 11 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Murphy
Copyright 2025 MIH AI B.V.

This product includes software developed at MIH AI B.V.

This product bundles the following third-party software:

browser_use/
MIT License
Copyright (c) 2024 Gregor Zunic
See browser_use/LICENSE for the full license text.
10 changes: 0 additions & 10 deletions browser_use/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ def _patched_del(self):
from browser_use.agent.views import ActionModel, ActionResult, AgentHistoryList
from browser_use.browser import BrowserProfile, BrowserSession
from browser_use.browser import BrowserSession as Browser
from browser_use.code_use.service import CodeAgent
from browser_use.dom.service import DomService
from browser_use.llm import models
from browser_use.llm.anthropic.chat import ChatAnthropic
Expand All @@ -65,15 +64,12 @@ def _patched_del(self):
from browser_use.llm.ollama.chat import ChatOllama
from browser_use.llm.openai.chat import ChatOpenAI
from browser_use.llm.vercel.chat import ChatVercel
from browser_use.sandbox import sandbox
from browser_use.tools.service import Controller, Tools

# Lazy imports mapping - only import when actually accessed
_LAZY_IMPORTS = {
# Agent service (heavy due to dependencies)
# 'Agent': ('browser_use.agent.service', 'Agent'),
# Code-use agent (Jupyter notebook-like execution)
'CodeAgent': ('browser_use.code_use.service', 'CodeAgent'),
'Agent': ('browser_use.agent.service', 'Agent'),
# System prompt (moderate weight due to agent.views imports)
'SystemPrompt': ('browser_use.agent.prompts', 'SystemPrompt'),
Expand Down Expand Up @@ -102,8 +98,6 @@ def _patched_del(self):
'ChatVercel': ('browser_use.llm.vercel.chat', 'ChatVercel'),
# LLM models module
'models': ('browser_use.llm.models', None),
# Sandbox execution
'sandbox': ('browser_use.sandbox', 'sandbox'),
}


Expand Down Expand Up @@ -131,8 +125,6 @@ def __getattr__(name: str):

__all__ = [
'Agent',
'CodeAgent',
# 'CodeAgent',
'BrowserSession',
'Browser', # Alias for BrowserSession
'BrowserProfile',
Expand All @@ -157,6 +149,4 @@ def __getattr__(name: str):
'Controller',
# LLM models module
'models',
# Sandbox execution
'sandbox',
]
3 changes: 1 addition & 2 deletions browser_use/agent/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,8 +1437,7 @@ def _force_done_on_severe_loop(self) -> None:
'Include everything you found out for the ultimate task in the done text.'
)
self.logger.info(
f'🛑 Force done on severe loop (repetition={ld.max_repetition_count}, '
f'stagnation={ld.consecutive_stagnant_pages})'
f'🛑 Force done on severe loop (repetition={ld.max_repetition_count}, stagnation={ld.consecutive_stagnant_pages})'
)
self._message_manager._add_context_message(UserMessage(content=msg))
self.AgentOutput = self.DoneAgentOutput
Expand Down
19 changes: 0 additions & 19 deletions browser_use/browser/cloud/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import httpx

from browser_use.browser.cloud.views import CloudBrowserAuthError, CloudBrowserError, CloudBrowserResponse, CreateBrowserRequest
from browser_use.sync.auth import CloudAuthConfig

logger = logging.getLogger(__name__)

Expand All @@ -37,17 +36,8 @@ async def create_browser(
"""
url = f'{self.api_base_url}/api/v2/browsers'

# Try to get API key from environment variable first, then auth config
api_token = os.getenv('BROWSER_USE_API_KEY')

if not api_token:
# Fallback to auth config file
try:
auth_config = CloudAuthConfig.load_from_file()
api_token = auth_config.api_token
except Exception:
pass

if not api_token:
raise CloudBrowserAuthError(
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service. You can also create an API key at https://cloud.browser-use.com/new-api-key'
Expand Down Expand Up @@ -124,17 +114,8 @@ async def stop_browser(

url = f'{self.api_base_url}/api/v2/browsers/{session_id}'

# Try to get API key from environment variable first, then auth config
api_token = os.getenv('BROWSER_USE_API_KEY')

if not api_token:
# Fallback to auth config file
try:
auth_config = CloudAuthConfig.load_from_file()
api_token = auth_config.api_token
except Exception:
pass

if not api_token:
raise CloudBrowserAuthError(
'No authentication token found. Please set BROWSER_USE_API_KEY environment variable to authenticate with the cloud service. You can also create an API key at https://cloud.browser-use.com/new-api-key'
Expand Down
2 changes: 1 addition & 1 deletion browser_use/browser/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ class BrowserLaunchArgs(BaseModel):
validation_alias=AliasChoices('browser_binary_path', 'chrome_binary_path'),
description='Path to the chromium-based browser executable to use.',
)
headless: bool | None = Field(default=None, description='Whether to run the browser in headless or windowed mode.')
headless: bool | None = Field(default=True, description='Whether to run the browser in headless or windowed mode.')
args: list[CliArgStr] = Field(
default_factory=list, description='List of *extra* CLI args to pass to the browser when launching.'
)
Expand Down
Loading
Loading