Skip to content
Open
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
5 changes: 4 additions & 1 deletion gateway/platforms/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,15 @@ async def connect(self) -> bool:
if not (bridge_dir / "node_modules").exists():
print(f"[{self.name}] Installing WhatsApp bridge dependencies...")
try:
# Read timeout from environment variable, default to 300 seconds (5 minutes)
# to accommodate slower systems like Unraid NAS
npm_install_timeout = int(os.environ.get("WHATSAPP_NPM_INSTALL_TIMEOUT", "300"))
install_result = subprocess.run(
["npm", "install", "--silent"],
cwd=str(bridge_dir),
capture_output=True,
text=True,
timeout=60,
timeout=npm_install_timeout,
)
if install_result.returncode != 0:
print(f"[{self.name}] npm install failed: {install_result.stderr}")
Expand Down
53 changes: 53 additions & 0 deletions tests/tools/test_browser_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,59 @@ def test_annotate_true_adds_flag(self):
assert "--annotate" in cmd_args



class TestBrowserVisionFullPage:
"""browser_vision supports full_page parameter."""

def test_schema_has_full_page_param(self):
from tools.browser_tool import BROWSER_TOOL_SCHEMAS

schema = next(s for s in BROWSER_TOOL_SCHEMAS if s["name"] == "browser_vision")
props = schema["parameters"]["properties"]
assert "full_page" in props
assert props["full_page"]["type"] == "boolean"
assert props["full_page"]["default"] is True

def test_full_page_default_includes_full_flag(self):
"""Default (full_page=True) includes --full in screenshot args."""
from tools.browser_tool import browser_vision

with (
patch("tools.browser_tool._run_browser_command") as mock_cmd,
patch("tools.browser_tool.call_llm") as mock_call_llm,
patch("tools.browser_tool._get_vision_model", return_value="test-model"),
):
mock_cmd.return_value = {"success": True, "data": {}}
try:
browser_vision("test", task_id="test")
except Exception:
pass

if mock_cmd.called:
args = mock_cmd.call_args[0]
cmd_args = args[2] if len(args) > 2 else []
assert "--full" in cmd_args

def test_full_page_false_omits_full_flag(self):
"""With full_page=False, screenshot command has no --full flag."""
from tools.browser_tool import browser_vision

with (
patch("tools.browser_tool._run_browser_command") as mock_cmd,
patch("tools.browser_tool.call_llm") as mock_call_llm,
patch("tools.browser_tool._get_vision_model", return_value="test-model"),
):
mock_cmd.return_value = {"success": True, "data": {}}
try:
browser_vision("test", full_page=False, task_id="test")
except Exception:
pass

if mock_cmd.called:
args = mock_cmd.call_args[0]
cmd_args = args[2] if len(args) > 2 else []
assert "--full" not in cmd_args

class TestBrowserVisionConfig:
def _setup_screenshot(self, tmp_path):
shots_dir = tmp_path / "browser_screenshots"
Expand Down
37 changes: 37 additions & 0 deletions tests/tools/test_file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,41 @@ def test_truncated_hint_with_nonzero_offset(self, mock_get):
assert "offset=100" in raw


class TestPatchSchema:
"""Tests for PATCH_SCHEMA to ensure required parameters are properly declared."""

def test_patch_schema_includes_all_required_params(self):
"""PATCH_SCHEMA should include all parameters that are conditionally required."""
from tools.file_tools import PATCH_SCHEMA

# Verify schema structure
assert "parameters" in PATCH_SCHEMA
assert "required" in PATCH_SCHEMA["parameters"]

# All parameters that are mode-specific should be in required list
required = PATCH_SCHEMA["parameters"]["required"]
assert "mode" in required
assert "path" in required
assert "old_string" in required
assert "new_string" in required
assert "patch" in required

# replace_all is optional (has default), so it should NOT be in required
assert "replace_all" not in required

def test_patch_schema_description_mentions_mode_specific_requirements(self):
"""PATCH_SCHEMA description should explain mode-specific requirements."""
from tools.file_tools import PATCH_SCHEMA

description = PATCH_SCHEMA.get("description", "")

# Description should mention mode-specific requirements
assert "mode-specific" in description.lower() or "IMPORTANT:" in description

# Should mention both modes
assert "mode='replace'" in description
assert "mode='patch'" in description




13 changes: 10 additions & 3 deletions tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,11 @@ def _update_session_activity(task_id: str):
"type": "boolean",
"default": False,
"description": "If true, overlay numbered [N] labels on interactive elements. Each [N] maps to ref @eN for subsequent browser commands. Useful for QA and spatial reasoning about page layout."
},
"full_page": {
"type": "boolean",
"default": True,
"description": "If true (default), capture the entire scrollable page. Set to false to capture only the current viewport — useful for long pages where full-page screenshots produce unreadable downscaled images."
}
},
"required": ["question"]
Expand Down Expand Up @@ -2121,7 +2126,7 @@ def browser_get_images(task_id: Optional[str] = None) -> str:
}, ensure_ascii=False)


def browser_vision(question: str, annotate: bool = False, task_id: Optional[str] = None) -> str:
def browser_vision(question: str, annotate: bool = False, full_page: bool = True, task_id: Optional[str] = None) -> str:
"""
Take a screenshot of the current page and analyze it with vision AI.

Expand All @@ -2136,6 +2141,7 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
Args:
question: What you want to know about the page visually
annotate: If True, overlay numbered [N] labels on interactive elements
full_page: If True (default), capture the full scrollable page. If False, capture only the current viewport.
task_id: Task identifier for session isolation

Returns:
Expand Down Expand Up @@ -2164,7 +2170,8 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
screenshot_args = []
if annotate:
screenshot_args.append("--annotate")
screenshot_args.append("--full")
if full_page:
screenshot_args.append("--full")
screenshot_args.append(str(screenshot_path))
result = _run_browser_command(
effective_task_id,
Expand Down Expand Up @@ -2623,7 +2630,7 @@ def check_browser_requirements() -> bool:
name="browser_vision",
toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_vision"],
handler=lambda args, **kw: browser_vision(question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id")),
handler=lambda args, **kw: browser_vision(question=args.get("question", ""), annotate=args.get("annotate", False), full_page=args.get("full_page", True), task_id=kw.get("task_id")),
check_fn=check_browser_requirements,
emoji="👁️",
)
Expand Down
12 changes: 6 additions & 6 deletions tools/file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,18 +908,18 @@ def _check_file_reqs():

PATCH_SCHEMA = {
"name": "patch",
"description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nReplace mode (default): find a unique string and replace it.\nPatch mode: apply V4A multi-file patches for bulk changes.",
"description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nIMPORTANT: Parameters are mode-specific:\n- For mode='replace': provide path, old_string, new_string (optionally replace_all)\n- For mode='patch': provide patch content",
"parameters": {
"type": "object",
"properties": {
"mode": {"type": "string", "enum": ["replace", "patch"], "description": "Edit mode: 'replace' for targeted find-and-replace, 'patch' for V4A multi-file patches", "default": "replace"},
"path": {"type": "string", "description": "File path to edit (required for 'replace' mode)"},
"old_string": {"type": "string", "description": "Text to find in the file (required for 'replace' mode). Must be unique in the file unless replace_all=true. Include enough surrounding context to ensure uniqueness."},
"new_string": {"type": "string", "description": "Replacement text (required for 'replace' mode). Can be empty string to delete the matched text."},
"path": {"type": "string", "description": "File path to edit (required when mode='replace')"},
"old_string": {"type": "string", "description": "Text to find in file (required when mode='replace'). Must be unique in file unless replace_all=true. Include enough surrounding context to ensure uniqueness."},
"new_string": {"type": "string", "description": "Replacement text (required when mode='replace'). Can be empty string to delete the matched text."},
"replace_all": {"type": "boolean", "description": "Replace all occurrences instead of requiring a unique match (default: false)", "default": False},
"patch": {"type": "string", "description": "V4A format patch content (required for 'patch' mode). Format:\n*** Begin Patch\n*** Update File: path/to/file\n@@ context hint @@\n context line\n-removed line\n+added line\n*** End Patch"}
"patch": {"type": "string", "description": "V4A format patch content (required when mode='patch'). Format:\n*** Begin Patch\n*** Update File: path/to/file\n@@ context hint @@\n context line\n-removed line\n+added line\n*** End Patch"}
},
"required": ["mode"]
"required": ["mode", "path", "old_string", "new_string", "patch"]
}
}

Expand Down
Loading