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
13 changes: 1 addition & 12 deletions camel/agents/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4072,22 +4072,11 @@ def _record_tool_calling(
)

# Record only the tool result message
func_records = self.update_memory(
self.update_memory(
func_msg,
OpenAIBackendRole.FUNCTION,
return_records=self._enable_snapshot_clean,
)

# Register tool output for snapshot cleaning if enabled
if self._enable_snapshot_clean and not mask_output and func_records:
serialized_result = self._serialize_tool_result(result_for_memory)
self._register_tool_output_for_cache(
func_name,
tool_call_id,
serialized_result,
cast(List[MemoryRecord], func_records),
)

if isinstance(result, ToolResult) and result.images:
try:
import base64
Expand Down
64 changes: 63 additions & 1 deletion camel/toolkits/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,23 @@
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========

import inspect
from typing import TYPE_CHECKING, Callable, List, Literal, Optional, TypeVar
from typing import (
TYPE_CHECKING,
Any,
Callable,
List,
Literal,
Optional,
TypeVar,
)

from camel.logger import get_logger
from camel.toolkits import FunctionTool
from camel.toolkits.output_processors import (
ToolOutputContext,
ToolOutputManager,
ToolOutputProcessor,
)
from camel.utils import AgentOpsMeta, Constants, with_timeout

if TYPE_CHECKING:
Expand Down Expand Up @@ -76,6 +89,55 @@ def __init__(self, timeout: Optional[float] = Constants.TIMEOUT_THRESHOLD):
raise ValueError("Timeout must be a positive number.")
self.timeout = timeout

# Initialize output management
self.output_manager = ToolOutputManager()

def register_output_processor(
self, processor: ToolOutputProcessor
) -> None:
r"""Register an output processor for tool results.

Args:
processor: The output processor to register.
"""
self.output_manager.register_processor(processor)

def process_tool_output(
self,
tool_name: str,
tool_call_id: str,
raw_result: Any,
agent_id: str,
timestamp: Optional[float] = None,
) -> ToolOutputContext:
r"""Process tool output through registered processors.

Args:
tool_name (str): Name of the tool that produced the output.
tool_call_id (str): Unique identifier for the tool call.
raw_result (Any): Raw output from the tool.
agent_id (str): ID of the agent that made the tool call.
timestamp (Optional[float]): Timestamp of the tool call.

Returns:
Processed tool output context.

Raises:
ValueError: If required parameters are missing or invalid.
"""
if not tool_name or not tool_call_id or not agent_id:
raise ValueError(
"tool_name, tool_call_id, and agent_id are required"
)

return self.output_manager.process_tool_output(
tool_name=tool_name,
tool_call_id=tool_call_id,
raw_result=raw_result,
agent_id=agent_id,
timestamp=timestamp,
)

# Add timeout to all callable methods in the toolkit
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Expand Down
112 changes: 89 additions & 23 deletions camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from camel.logger import get_logger
from camel.toolkits.base import BaseToolkit, RegisteredAgentToolkit
from camel.toolkits.function_tool import FunctionTool
from camel.toolkits.output_processors import SnapshotCleaningProcessor
from camel.utils.tool_result import ToolResult

from .config_loader import ConfigLoader
Expand Down Expand Up @@ -329,62 +330,68 @@ def __init__(
cdp_url: Optional[str] = None,
cdp_keep_current_page: bool = False,
full_visual_mode: bool = False,
clean_snapshots: bool = False,
download_dir: Optional[str] = None,
) -> None:
r"""Initialize the HybridBrowserToolkit.

Args:
headless (bool): Whether to run browser in headless mode.
Defaults to True.
Defaults to True.
user_data_dir (Optional[str]): Directory for user data
persistence. Defaults to None.
persistence. Defaults to None.
stealth (bool): Whether to enable stealth mode. Defaults to
False.
cache_dir (str): Directory for caching. Defaults to "tmp/".
enabled_tools (Optional[List[str]]): List of enabled tools.
Defaults to None.
Defaults to None.
browser_log_to_file (bool): Whether to log browser actions to
file. Defaults to False.
file. Defaults to False.
log_dir (Optional[str]): Custom directory path for log files.
If None, defaults to "browser_log". Defaults to None.
If None, defaults to "browser_log". Defaults to None.
session_id (Optional[str]): Session identifier. Defaults to None.
default_start_url (str): Default URL to start with. Defaults
to "https://google.com/".
to "https://google.com/".
default_timeout (Optional[int]): Default timeout in
milliseconds. Defaults to None.
milliseconds. Defaults to None.
short_timeout (Optional[int]): Short timeout in milliseconds.
Defaults to None.
Defaults to None.
navigation_timeout (Optional[int]): Navigation timeout in
milliseconds. Defaults to None.
milliseconds. Defaults to None.
network_idle_timeout (Optional[int]): Network idle timeout in
milliseconds. Defaults to None.
milliseconds. Defaults to None.
screenshot_timeout (Optional[int]): Screenshot timeout in
milliseconds. Defaults to None.
milliseconds. Defaults to None.
page_stability_timeout (Optional[int]): Page stability timeout
in milliseconds. Defaults to None.
in milliseconds. Defaults to None.
dom_content_loaded_timeout (Optional[int]): DOM content loaded
timeout in milliseconds. Defaults to None.
timeout in milliseconds. Defaults to None.
download_timeout (Optional[int]): Download timeout in
milliseconds. Defaults to None.
milliseconds. Defaults to None.
viewport_limit (bool): Whether to filter page snapshot
elements to only those visible in the current viewport.
When True, only elements within the current viewport
bounds will be included in snapshots.
When False (default), all elements on the page are
included. Defaults to False.
connect_over_cdp (bool): Whether to connect to an existing
browser via Chrome DevTools Protocol. Defaults to False.
browser via Chrome DevTools Protocol. Defaults to False.
cdp_url (Optional[str]): WebSocket endpoint URL for CDP
connection (e.g., 'ws://localhost:9222/devtools/browser/...').
Required when connect_over_cdp is True. Defaults to None.
connection (e.g., 'ws://localhost:9222/devtools/browser/...').
Required when connect_over_cdp is True. Defaults to None.
cdp_keep_current_page (bool): When True and using CDP mode,
won't create new pages but use the existing one. Defaults to False.
won't create new pages but use the existing one. Defaults to
False.
full_visual_mode (bool): When True, browser actions like click,
browser_open, visit_page, etc. will not return snapshots.
Defaults to False.
browser_open, visit_page, etc. will not return snapshots.
Defaults to False.
clean_snapshots (bool): When True, automatically cleans verbose
DOM snapshots to reduce context usage while preserving
essential information. Removes redundant markers and references
from browser tool outputs. Defaults to False.
download_dir (Optional[str]): Directory path where downloaded
files will be saved when using browser_download_file tool.
Defaults to None.
files will be saved when using browser_download_file tool.
Defaults to None.
"""
super().__init__()
RegisteredAgentToolkit.__init__(self)
Expand Down Expand Up @@ -438,6 +445,7 @@ def __init__(
self._session_id = toolkit_config.session_id or "default"
self._viewport_limit = browser_config.viewport_limit
self._full_visual_mode = browser_config.full_visual_mode
self._clean_snapshots = clean_snapshots

self._default_timeout = browser_config.default_timeout
self._short_timeout = browser_config.short_timeout
Expand All @@ -464,6 +472,15 @@ def __init__(

logger.info(f"Enabled tools: {self.enabled_tools}")

# Setup snapshot cleaning if enabled
if self._clean_snapshots:
snapshot_processor = SnapshotCleaningProcessor()
self.register_output_processor(snapshot_processor)
logger.info(
"Snapshot cleaning enabled - DOM snapshots will "
"be automatically cleaned"
)

self._ws_wrapper: Optional[WebSocketBrowserWrapper] = None
self._ws_config = self.config_loader.to_ws_config()

Expand All @@ -480,6 +497,38 @@ async def _get_ws_wrapper(self) -> WebSocketBrowserWrapper:
raise RuntimeError("Failed to initialize WebSocket wrapper")
return self._ws_wrapper

def _clean_snapshot_if_enabled(
self, snapshot: str, tool_name: str = "browser_ts"
) -> str:
r"""Clean snapshot content if snapshot cleaning is enabled.

Args:
snapshot: The raw snapshot content to clean.
tool_name: The name of the tool that generated the snapshot.

Returns:
The cleaned snapshot if cleaning is enabled, otherwise the
original snapshot.
"""
if not self._clean_snapshots or not snapshot:
return snapshot

try:
# Process through the output manager
processed_context = self.process_tool_output(
tool_name=tool_name,
tool_call_id="snapshot_clean",
raw_result=snapshot,
agent_id=getattr(self, '_session_id', 'default'),
)

return processed_context.raw_result
except Exception as e:
logger.warning(
f"Failed to clean snapshot: {e}, returning original"
)
return snapshot

def __del__(self):
r"""Cleanup browser resources on garbage collection."""
try:
Expand Down Expand Up @@ -561,6 +610,11 @@ async def _build_action_response(
# In full_visual_mode, clear the snapshot
if self._full_visual_mode:
response["snapshot"] = ""
# Clean snapshot if cleaning is enabled
elif self._clean_snapshots and "snapshot" in response:
response["snapshot"] = self._clean_snapshot_if_enabled(
response["snapshot"], "browser_action"
)

# Always include note field for consistency (empty if not present)
if include_note:
Expand Down Expand Up @@ -751,7 +805,11 @@ async def browser_get_page_snapshot(self) -> str:
"""
try:
ws_wrapper = await self._get_ws_wrapper()
return await ws_wrapper.get_page_snapshot(self._viewport_limit)
result = await ws_wrapper.get_page_snapshot(self._viewport_limit)
# Clean snapshot if enabled
return self._clean_snapshot_if_enabled(
result, "browser_get_page_snapshot"
)
except Exception as e:
logger.error(f"Failed to get page snapshot: {e}")
return f"Error capturing snapshot: {e}"
Expand Down Expand Up @@ -1437,6 +1495,13 @@ async def browser_console_exec(self, code: str) -> Dict[str, Any]:
result = await ws_wrapper.console_exec(code)

tab_info = await ws_wrapper.get_tab_info()

# Clean snapshot if enabled
if "snapshot" in result:
result["snapshot"] = self._clean_snapshot_if_enabled(
result["snapshot"], "browser_console_exec"
)

result.update(
{
"tabs": tab_info,
Expand Down Expand Up @@ -2089,6 +2154,7 @@ def clone_for_new_session(
dom_content_loaded_timeout=self._dom_content_loaded_timeout,
viewport_limit=self._viewport_limit,
full_visual_mode=self._full_visual_mode,
clean_snapshots=self._clean_snapshots,
)

# Tools not available in full_visual_mode (require ref, no pixel alt)
Expand Down
Loading
Loading