diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 61ee83f65c..afbd20ead9 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -915,6 +915,202 @@ def _summarize_tool_result(self, text: str, limit: int = 160) -> str: return normalized return normalized[: max(0, limit - 3)].rstrip() + "..." + def _clean_snapshot_line(self, line: str) -> str: + r"""Clean a single snapshot line by removing prefixes and references. + + This method handles snapshot lines in the format: + - [prefix] "quoted text" [attributes] [ref=...]: description + + It preserves: + - Quoted text content (including brackets inside quotes) + - Description text after the colon + + It removes: + - Line prefixes (e.g., "- button", "- tooltip", "generic:") + - Attribute markers (e.g., [disabled], [ref=e47]) + - Lines with only element types + - All indentation + + Args: + line: The original line content. + + Returns: + The cleaned line content, or empty string if line should be removed. + """ + # Remove all leading whitespace (indentation) + original = line.strip() + if not original: + return '' + + # Check if line is just a line prefix (element type) with optional colon + # Examples: "- generic:", "- img", "generic:", "img", "button:" + # Matches "- word" or "word:" or "- word:" or just "word" + if re.match(r'^(?:-\s+)?\w+\s*:?\s*$', original): + return '' # Remove lines with only element types + + # Step 1: Remove line prefix using regex + # Matches: "- button ", "- generic: ", "tooltip: ", "- img ", etc. + line = re.sub(r'^(?:-\s+)?\w+[\s:]+', '', original) + + # Step 2: Remove bracket markers outside of quotes + # Strategy: protect quoted content, remove brackets, restore quotes + + # Save all quoted content temporarily + quoted_parts = [] + + def save_quoted(match): + quoted_parts.append(match.group(0)) + return f'__QUOTED_{len(quoted_parts)-1}__' + + # Protect quoted content (double quotes) + line = re.sub(r'"[^"]*"', save_quoted, line) + + # Remove all bracket markers (quotes are protected) + line = re.sub(r'\s*\[[^\]]+\]\s*', ' ', line) + + # Restore quoted content + for i, quoted in enumerate(quoted_parts): + line = line.replace(f'__QUOTED_{i}__', quoted) + + # Step 3: Clean up whitespace and formatting + line = re.sub(r'\s+', ' ', line).strip() + line = re.sub(r'\s*:\s*', ': ', line) + + # Remove leading colon if that's all that remains + if line == ':' or line.startswith(': '): + line = line.lstrip(': ').strip() + + # Return empty string if nothing meaningful remains + if not line: + return '' + + return line + + def _clean_snapshot_content(self, content: str) -> str: + r"""Clean snapshot content by removing prefixes, references, and + deduplicating lines. + + This method identifies snapshot lines (containing element keywords or + references) and cleans them while preserving non-snapshot content. + It also handles JSON-formatted tool outputs with snapshot fields. + + Args: + content: The original snapshot content. + + Returns: + The cleaned content with deduplicated lines. + """ + # Try to parse as JSON first + try: + import json + + data = json.loads(content) + modified = False + + # Recursively clean snapshot fields in JSON + def clean_json_value(obj): + nonlocal modified + if isinstance(obj, dict): + result = {} + for key, value in obj.items(): + if key == 'snapshot' and isinstance(value, str): + # Found a snapshot field, clean it + # Decode escape sequences (e.g., \n -> actual newline) + try: + decoded_value = value.encode().decode('unicode_escape') + except: + decoded_value = value + + # Check if cleaning is needed + needs_cleaning = ( + '- ' in decoded_value or + '[ref=' in decoded_value or + any(elem + ':' in decoded_value for elem in [ + 'generic', 'img', 'banner', 'list', + 'listitem', 'search', 'navigation' + ]) + ) + + if needs_cleaning: + cleaned_snapshot = self._clean_text_snapshot( + decoded_value + ) + result[key] = cleaned_snapshot + modified = True + else: + result[key] = value + else: + result[key] = clean_json_value(value) + return result + elif isinstance(obj, list): + return [clean_json_value(item) for item in obj] + else: + return obj + + cleaned_data = clean_json_value(data) + + if modified: + return json.dumps(cleaned_data, ensure_ascii=False, indent=4) + else: + return content + + except (json.JSONDecodeError, TypeError): + # Not JSON, process as plain text + return self._clean_text_snapshot(content) + + def _clean_text_snapshot(self, content: str) -> str: + r"""Clean plain text snapshot content. + + This method: + - Removes all indentation + - Deletes empty lines + - Deduplicates all lines + - Cleans snapshot-specific markers + + Args: + content: The original snapshot text. + + Returns: + The cleaned content with deduplicated lines, no indentation, and no empty lines. + """ + lines = content.split('\n') + cleaned_lines = [] + seen = set() # For deduplication across ALL lines + + for line in lines: + # Strip indentation from every line + stripped_line = line.strip() + + # Skip empty lines + if not stripped_line: + continue + + # Skip metadata lines (like "- /url:", "- /ref:", etc.) + if re.match(r'^-?\s*/\w+\s*:', stripped_line): + continue + + # Check if this is a snapshot line using regex + # Matches lines with: [ref=...], "- element", "element:", "- element:", etc. + is_snapshot_line = ( + '[ref=' in stripped_line or + re.match(r'^(?:-\s+)?\w+(?:[\s:]|$)', stripped_line) + ) + + if is_snapshot_line: + # Clean snapshot line + cleaned = self._clean_snapshot_line(stripped_line) + # Only add if not empty and not duplicate + if cleaned and cleaned not in seen: + cleaned_lines.append(cleaned) + seen.add(cleaned) + else: + # Non-snapshot line: remove indentation, deduplicate + if stripped_line not in seen: + cleaned_lines.append(stripped_line) + seen.add(stripped_line) + + return '\n'.join(cleaned_lines) + def _register_tool_output_for_cache( self, func_name: str, @@ -956,11 +1152,22 @@ def _cache_tool_output_entry(self, entry: _ToolOutputHistoryEntry) -> None: if self._tool_output_cache_manager is None or not entry.record_uuids: return + # Check if result contains snapshot markers and clean if necessary + result_to_cache = entry.result_text + if '- ' in result_to_cache and '[ref=' in result_to_cache: + # Likely contains snapshot with references, clean it + result_to_cache = self._clean_snapshot_content(result_to_cache) + logger.debug( + "Cleaned snapshot references from tool output '%s' (%s)", + entry.tool_name, + entry.tool_call_id, + ) + try: cache_id, cache_path = self._tool_output_cache_manager.save( entry.tool_name, entry.tool_call_id, - entry.result_text, + result_to_cache, ) except Exception as exc: # pragma: no cover - defensive logger.warning( @@ -986,7 +1193,7 @@ def _cache_tool_output_entry(self, entry: _ToolOutputHistoryEntry) -> None: }, content="", func_name=entry.tool_name, - result=self._build_cache_reference_text(entry, cache_id), + result=result_to_cache, # Use cleaned content directly tool_call_id=entry.tool_call_id, ) diff --git a/camel/models/base_model.py b/camel/models/base_model.py index 023f543a1e..dbb9a44459 100644 --- a/camel/models/base_model.py +++ b/camel/models/base_model.py @@ -117,8 +117,7 @@ def __init__( self._max_retries = max_retries # Initialize logging configuration self._log_enabled = ( - os.environ.get("CAMEL_MODEL_LOG_ENABLED", "False").lower() - == "true" + os.environ.get("CAMEL_MODEL_LOG_ENABLED", "True").lower() == "true" ) self._log_dir = os.environ.get("CAMEL_LOG_DIR", "camel_logs") diff --git a/camel/models/moonshot_model.py b/camel/models/moonshot_model.py index ba27e7f47b..ecaf93a9ca 100644 --- a/camel/models/moonshot_model.py +++ b/camel/models/moonshot_model.py @@ -84,7 +84,7 @@ def __init__( model_type: Union[ModelType, str], model_config_dict: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, - url: Optional[str] = "https://api.moonshot.ai/v1", + url: Optional[str] = None, token_counter: Optional[BaseTokenCounter] = None, timeout: Optional[float] = None, max_retries: int = 3, @@ -93,7 +93,12 @@ def __init__( if model_config_dict is None: model_config_dict = MoonshotConfig().as_dict() api_key = api_key or os.environ.get("MOONSHOT_API_KEY") - url = url or os.environ.get("MOONSHOT_API_BASE_URL") + # Preserve default URL if not provided + if url is None: + url = ( + os.environ.get("MOONSHOT_API_BASE_URL") + or "https://api.moonshot.ai/v1" + ) timeout = timeout or float(os.environ.get("MODEL_TIMEOUT", 180)) super().__init__( model_type=model_type, @@ -130,7 +135,9 @@ def _prepare_request( request_config = copy.deepcopy(self.model_config_dict) if tools: - request_config["tools"] = tools + # Clean tools to remove null types (Moonshot API incompatibility) + cleaned_tools = self._clean_tool_schemas(tools) + request_config["tools"] = cleaned_tools elif response_format: # Use the same approach as DeepSeek for structured output try_modify_message_with_format(messages[-1], response_format) @@ -138,6 +145,83 @@ def _prepare_request( return request_config + def _clean_tool_schemas( + self, tools: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + r"""Clean tool schemas to remove null types for Moonshot compatibility. + + Moonshot API doesn't accept {"type": "null"} in anyOf schemas. + This method removes null type definitions from parameters. + + Args: + tools (List[Dict[str, Any]]): Original tool schemas. + + Returns: + List[Dict[str, Any]]: Cleaned tool schemas. + """ + import copy + + def remove_null_from_schema(schema: Any) -> Any: + """Recursively remove null types from schema.""" + if isinstance(schema, dict): + # Create a copy to avoid modifying the original + result = {} + + for key, value in schema.items(): + if key == 'type' and isinstance(value, list): + # Handle type arrays like ["string", "null"] + filtered_types = [t for t in value if t != 'null'] + if len(filtered_types) == 1: + # Single type remains, convert to string + result[key] = filtered_types[0] + elif len(filtered_types) > 1: + # Multiple types remain, keep as array + result[key] = filtered_types + else: + # All were null, use string as fallback + result[key] = 'string' + elif key == 'anyOf': + # Handle anyOf with null types + filtered = [ + item + for item in value + if not ( + isinstance(item, dict) + and item.get('type') == 'null' + ) + ] + if len(filtered) == 1: + # If only one type remains, flatten it + return remove_null_from_schema(filtered[0]) + elif len(filtered) > 1: + result[key] = [ + remove_null_from_schema(item) + for item in filtered + ] + else: + # All were null, return string type as fallback + return {"type": "string"} + else: + # Recursively process other values + result[key] = remove_null_from_schema(value) + + return result + elif isinstance(schema, list): + return [remove_null_from_schema(item) for item in schema] + else: + return schema + + cleaned_tools = copy.deepcopy(tools) + for tool in cleaned_tools: + if 'function' in tool and 'parameters' in tool['function']: + params = tool['function']['parameters'] + if 'properties' in params: + params['properties'] = remove_null_from_schema( + params['properties'] + ) + + return cleaned_tools + @observe() async def _arun( self, diff --git a/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py b/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py index b5f8465deb..ba01abe51e 100644 --- a/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +++ b/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py @@ -555,7 +555,7 @@ async def browser_get_page_snapshot(self) -> str: async def browser_get_som_screenshot( self, read_image: bool = True, - instruction: Optional[str] = None, + instruction: str = "", ) -> str: r"""Captures a screenshot with interactive elements highlighted. @@ -645,10 +645,9 @@ async def browser_get_som_screenshot( from PIL import Image img = Image.open(file_path) - inst = instruction if instruction is not None else "" message = BaseMessage.make_user_message( role_name="User", - content=inst, + content=instruction, image_list=[img], ) @@ -722,50 +721,33 @@ async def browser_click(self, *, ref: str) -> Dict[str, Any]: async def browser_type( self, *, - ref: Optional[str] = None, - text: Optional[str] = None, - inputs: Optional[List[Dict[str, str]]] = None, + ref: str, + text: str, ) -> Dict[str, Any]: - r"""Types text into one or more input elements on the page. - - This method supports two modes: - 1. Single input mode (backward compatible): Provide 'ref' and 'text' - 2. Multiple inputs mode: Provide 'inputs' as a list of dictionaries - with 'ref' and 'text' keys + r"""Types text into an input element on the page. Args: - ref (Optional[str]): The `ref` ID of the input element, from a - snapshot. Required when using single input mode. - text (Optional[str]): The text to type into the element. Required - when using single input mode. - inputs (Optional[List[Dict[str, str]]]): List of dictionaries, - each containing 'ref' and 'text' keys for typing into multiple - elements. Example: [{'ref': '1', 'text': 'username'}, - {'ref': '2', 'text': 'password'}] + ref (str): The `ref` ID of the input element, from a snapshot. + text (str): The text to type into the element. Returns: Dict[str, Any]: A dictionary with the result of the action: - - "result" (str): Confirmation of the action. + - "result" (str): Confirmation message describing the action. - "snapshot" (str): A textual snapshot of the page after - typing. + typing, showing the updated state of interactive elements. - "tabs" (List[Dict]): Information about all open tabs. - - "current_tab" (int): Index of the active tab. - - "total_tabs" (int): Total number of open tabs. - - "details" (Dict[str, Any]): When using multiple inputs, - contains success/error status for each ref. + - "current_tab" (int): Index of the active tab (zero-based). + + Example: + >>> browser_type( + ... ref="3", + ... text="hello@example.com" + ... ) + Typed text into element 3 """ try: ws_wrapper = await self._get_ws_wrapper() - - if ref is not None and text is not None: - result = await ws_wrapper.type(ref, text) - elif inputs is not None: - result = await ws_wrapper.type_multiple(inputs) - else: - raise ValueError( - "Either provide 'ref' and 'text' for single input, " - "or 'inputs' for multiple inputs" - ) + result = await ws_wrapper.type(ref, text) tab_info = await ws_wrapper.get_tab_info() result.update( diff --git a/camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts b/camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts index 1141c695b9..96f02e8646 100644 --- a/camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +++ b/camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts @@ -646,7 +646,7 @@ export class HybridBrowserSession { // Handle multiple inputs if provided if (inputs && inputs.length > 0) { const results: Record = {}; - + for (const input of inputs) { const singleResult = await this.performType(page, input.ref, input.text); results[input.ref] = { @@ -654,17 +654,35 @@ export class HybridBrowserSession { error: singleResult.error }; } - - // Check if all inputs were successful - const allSuccess = Object.values(results).every(r => r.success); - const errors = Object.entries(results) + + // Check how many inputs were successful + const successfulRefs = Object.entries(results) + .filter(([_, r]) => r.success) + .map(([ref, _]) => ref); + const failedRefs = Object.entries(results) .filter(([_, r]) => !r.success) - .map(([ref, r]) => `${ref}: ${r.error}`) - .join('; '); - + .map(([ref, r]) => `${ref}: ${r.error}`); + + const hasAnySuccess = successfulRefs.length > 0; + const allSuccess = failedRefs.length === 0; + + // Build detailed error message + let errorMessage: string | undefined; + if (!allSuccess) { + const parts: string[] = []; + if (successfulRefs.length > 0) { + parts.push(`Successfully typed into: ${successfulRefs.join(', ')}`); + } + if (failedRefs.length > 0) { + parts.push(`Failed: ${failedRefs.join('; ')}`); + } + errorMessage = parts.join('. '); + } + + // Return success if at least one input succeeded return { - success: allSuccess, - error: allSuccess ? undefined : `Some inputs failed: ${errors}`, + success: hasAnySuccess, + error: errorMessage, details: results }; } @@ -1137,19 +1155,38 @@ export class HybridBrowserSession { const typeStart = Date.now(); const typeResult = await this.performType(page, action.ref, action.text, action.inputs); - + + // For single input mode: throw error if failed + // For multiple inputs mode: only throw if ALL inputs failed if (!typeResult.success) { - throw new Error(`Type failed: ${typeResult.error}`); + // Check if this is multiple inputs mode + if (typeResult.details) { + const hasAnySuccess = Object.values(typeResult.details).some((r: any) => r.success); + if (!hasAnySuccess) { + // All inputs failed, throw error + throw new Error(`Type failed: ${typeResult.error}`); + } + // Some inputs succeeded, continue with partial success + } else { + // Single input mode failed, throw error + throw new Error(`Type failed: ${typeResult.error}`); + } } - + // Set custom message and details if multiple inputs were used if (typeResult.details) { const successCount = Object.values(typeResult.details).filter((r: any) => r.success).length; const totalCount = Object.keys(typeResult.details).length; - customMessage = `Typed text into ${successCount}/${totalCount} elements`; + if (typeResult.error) { + // Partial success - include error in message + customMessage = `Typed text into ${successCount}/${totalCount} elements. ${typeResult.error}`; + } else { + // Full success + customMessage = `Typed text into ${successCount}/${totalCount} elements`; + } actionDetails = typeResult.details; } - + // Capture diff snapshot if present if (typeResult.diffSnapshot) { if (!actionDetails) { @@ -1157,7 +1194,7 @@ export class HybridBrowserSession { } actionDetails.diffSnapshot = typeResult.diffSnapshot; } - + actionExecutionTime = Date.now() - typeStart; break; } diff --git a/camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js b/camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js index a9fbf4cc62..c630233b1e 100644 --- a/camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +++ b/camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js @@ -10,7 +10,8 @@ class WebSocketBrowserServer { async start() { return new Promise((resolve, reject) => { - this.server = new WebSocket.Server({ + this.server = new WebSocket.Server({ + host: '127.0.0.1', port: this.port, maxPayload: 50 * 1024 * 1024 // 50MB limit instead of default 1MB }, () => { diff --git a/camel/types/enums.py b/camel/types/enums.py index e66d213f1f..5fbd24bc84 100644 --- a/camel/types/enums.py +++ b/camel/types/enums.py @@ -326,7 +326,7 @@ class ModelType(UnifiedModelType, Enum): MOONSHOT_V1_8K = "moonshot-v1-8k" MOONSHOT_V1_32K = "moonshot-v1-32k" MOONSHOT_V1_128K = "moonshot-v1-128k" - MOONSHOT_KIMI_K2 = "kimi-k2-0711-preview" + MOONSHOT_KIMI_K2 = "kimi-latest" # SiliconFlow models support tool calling SILICONFLOW_DEEPSEEK_V2_5 = "deepseek-ai/DeepSeek-V2.5" diff --git a/examples/toolkits/hybrid_browser_toolkit_example.py b/examples/toolkits/hybrid_browser_toolkit_example.py index 28b7a3d62e..3f891064ce 100644 --- a/examples/toolkits/hybrid_browser_toolkit_example.py +++ b/examples/toolkits/hybrid_browser_toolkit_example.py @@ -33,11 +33,12 @@ logging.getLogger('camel.toolkits.hybrid_browser_toolkit').setLevel( logging.DEBUG ) -USER_DATA_DIR = "User_Data" +USER_DATA_DIR = "/Users/puzhen/Desktop/pre/camel_project/camel/UserData" model_backend = ModelFactory.create( - model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_4O, + model_platform=ModelPlatformType.MOONSHOT, + model_type=ModelType.MOONSHOT_KIMI_K2, + url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL model_config_dict={"temperature": 0.0, "top_p": 1}, ) @@ -67,7 +68,6 @@ "browser_type", "browser_switch_tab", "browser_enter", - # "browser_get_som_screenshot", # remove it to achieve faster operation # "browser_press_key", # "browser_console_view", # "browser_console_exec", @@ -86,6 +86,7 @@ print(f"Custom tools: {web_toolkit_custom.enabled_tools}") # Use the custom toolkit for the actual task agent = ChatAgent( + enable_tool_output_cache=True, model=model_backend, tools=[*web_toolkit_custom.get_tools()], toolkits_to_register_agent=[web_toolkit_custom], @@ -105,6 +106,7 @@ async def main() -> None: + # await web_toolkit_custom.browser_open() try: response = await agent.astep(TASK_PROMPT) print("Task:", TASK_PROMPT) diff --git a/examples/workforce/eigent.py b/examples/workforce/eigent.py index 4b749d443e..60b5be0fce 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -51,7 +51,6 @@ WhatsAppToolkit, ) from camel.types import ModelPlatformType, ModelType -from camel.utils.commons import api_keys_required logger = get_logger(__name__) @@ -289,13 +288,13 @@ def developer_agent_factory( ) -@api_keys_required( - [ - (None, 'GOOGLE_API_KEY'), - (None, 'SEARCH_ENGINE_ID'), - (None, 'EXA_API_KEY'), - ] -) +# @api_keys_required( +# [ +# (None, 'GOOGLE_API_KEY'), +# (None, 'SEARCH_ENGINE_ID'), +# (None, 'EXA_API_KEY'), +# ] +# ) def search_agent_factory( model: BaseModelBackend, task_id: str, @@ -313,6 +312,7 @@ def search_agent_factory( custom_tools = [ "browser_open", + "browser_select", "browser_close", "browser_back", "browser_forward", @@ -321,8 +321,10 @@ def search_agent_factory( "browser_enter", "browser_switch_tab", "browser_visit_page", - "browser_get_som_screenshot", + "browser_get_page_snapshot", ] + USER_DATA_DIR = "/Users/puzhen/Desktop/pre/camel_project/camel/UserData" + web_toolkit_custom = HybridBrowserToolkit( headless=False, enabled_tools=custom_tools, @@ -331,7 +333,9 @@ def search_agent_factory( session_id=agent_id, viewport_limit=False, cache_dir=WORKING_DIRECTORY, - default_start_url="https://search.brave.com/", + default_start_url="about:blank", + user_data_dir=USER_DATA_DIR, + log_dir="task2", ) # Initialize toolkits @@ -346,7 +350,7 @@ def search_agent_factory( ) terminal_toolkit = message_integration.register_toolkits(terminal_toolkit) note_toolkit = message_integration.register_toolkits(note_toolkit) - search_toolkit = message_integration.register_functions([search_toolkit]) + # search_toolkit = message_integration.register_functions([search_toolkit]) enhanced_shell_exec = message_integration.register_functions( [terminal_toolkit_basic.shell_exec] ) @@ -356,11 +360,11 @@ def search_agent_factory( *enhanced_shell_exec, HumanToolkit().ask_human_via_console, *note_toolkit.get_tools(), - *search_toolkit, + # *search_toolkit, *terminal_toolkit.get_tools(), ] - system_message = f""" + system_message = """ You are a Senior Research Analyst, a key member of a multi-agent team. Your primary responsibility is to conduct expert-level web research to gather, @@ -368,25 +372,6 @@ def search_agent_factory( operate with precision, efficiency, and a commitment to data quality. - -You collaborate with the following agents who can work in parallel: -- **Developer Agent**: Writes and executes code, handles technical -implementation. -- **Document Agent**: Creates and manages documents and presentations. -- **Multi-Modal Agent**: Processes and generates images and audio. -Your research is the foundation of the team's work. Provide them with -comprehensive and well-documented information. - - - -- **System**: {platform.system()} ({platform.machine()}) -- **Working Directory**: `{WORKING_DIRECTORY}`. All local file operations must - occur here, but you can access files from any place in the file system. For - all file system operations, you MUST use absolute paths to ensure precision - and avoid ambiguity. -- **Current Date**: {datetime.date.today()}. - - - You MUST use the note-taking tools to record your findings. This is a critical part of your role. Your notes are the primary source of @@ -402,18 +387,6 @@ def search_agent_factory( you have discovered. High-quality, detailed notes are essential for the team's success. -- You MUST only use URLs from trusted sources. A trusted source is a URL - that is either: - 1. Returned by a search tool (like `search_google`, `search_bing`, - or `search_exa`). - 2. Found on a webpage you have visited. -- You are strictly forbidden from inventing, guessing, or constructing URLs - yourself. Fabricating URLs will be considered a critical error. - -- You MUST NOT answer from your own knowledge. All information - MUST be sourced from the web using the available tools. If you don't know - something, find it out using your tools. - - When you complete your task, your final response must be a comprehensive summary of your findings, presented in a clear, detailed, and easy-to-read format. Avoid using markdown tables for presenting data; @@ -432,11 +405,9 @@ def search_agent_factory( -- Initial Search: You MUST start with a search engine like `search_google` or - `search_bing` to get a list of relevant URLs for your research, the URLs - here will be used for `browser_visit_page`. - Browser-Based Exploration: Use the rich browser related toolset to investigate websites. + - **Navigation and Exploration**: Use `browser_visit_page` to open a URL. `browser_visit_page` provides a snapshot of currently visible interactive elements, not the full page text. To see more content on @@ -447,30 +418,30 @@ def search_agent_factory( operation, only use it when visual analysis is necessary. - **Interaction**: Use `browser_type` to fill out forms and `browser_enter` to submit or confirm search. -- Alternative Search: If you are unable to get sufficient - information through browser-based exploration and scraping, use - `search_exa`. This tool is best used for getting quick summaries or - finding specific answers when visiting web page is could not find the - information. - In your response, you should mention the URLs you have visited and processed. - When encountering verification challenges (like login, CAPTCHAs or robot checks), you MUST request help using the human toolkit. +- When encountering cookies page, you need to click accept all. """ - return ChatAgent( + agent = ChatAgent( system_message=BaseMessage.make_assistant_message( role_name="Search Agent", content=system_message, ), model=model, + enable_tool_output_cache=True, toolkits_to_register_agent=[web_toolkit_custom], tools=tools, - prune_tool_calls_from_memory=True, + # prune_tool_calls_from_memory=True, ) + # Return both agent and toolkit for cleanup purposes + return agent, web_toolkit_custom + def document_agent_factory( model: BaseModelBackend, @@ -490,7 +461,7 @@ def document_agent_factory( mark_it_down_toolkit = MarkItDownToolkit() excel_toolkit = ExcelToolkit(working_directory=WORKING_DIRECTORY) note_toolkit = NoteTakingToolkit(working_directory=WORKING_DIRECTORY) - search_toolkit = SearchToolkit().search_exa + # search_toolkit = SearchToolkit().search_exa terminal_toolkit = TerminalToolkit(safe_mode=True, clone_current_env=False) # Add messaging to toolkits @@ -503,7 +474,7 @@ def document_agent_factory( ) excel_toolkit = message_integration.register_toolkits(excel_toolkit) note_toolkit = message_integration.register_toolkits(note_toolkit) - search_toolkit = message_integration.register_functions([search_toolkit]) + # search_toolkit = message_integration.register_functions([search_toolkit]) terminal_toolkit = message_integration.register_toolkits(terminal_toolkit) tools = [ @@ -513,7 +484,7 @@ def document_agent_factory( *mark_it_down_toolkit.get_tools(), *excel_toolkit.get_tools(), *note_toolkit.get_tools(), - *search_toolkit, + # *search_toolkit, *terminal_toolkit.get_tools(), ] @@ -654,6 +625,7 @@ def document_agent_factory( role_name="Document Agent", content=system_message, ), + enable_tool_output_cache=True, model=model, tools=tools, ) @@ -1016,7 +988,8 @@ async def main(): ) # Create agents using factory functions - search_agent = search_agent_factory(model_backend, task_id) + # search_agent_factory now returns (agent, browser_toolkit) + search_agent, _ = search_agent_factory(model_backend, task_id) developer_agent = developer_agent_factory( model_backend_reason, task_id, @@ -1097,8 +1070,7 @@ async def main(): human_task = Task( content=( """ -search 10 different papers related to llm agent and write a html report about -them. +Find a recipe for a vegetarian lasagna under 600 calories per serving that has a prep time of less than 1 hour. in https://www.allrecipes.com/ """ ), id='0', diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py new file mode 100644 index 0000000000..0ee7fd32d8 --- /dev/null +++ b/examples/workforce/run_eigent.py @@ -0,0 +1,327 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= + +import asyncio +import datetime +import os +import platform + +# Import agent factories from eigent.py +from eigent import ( + # developer_agent_factory, + # document_agent_factory, + # multi_modal_agent_factory, + search_agent_factory, +) + +from camel.agents.chat_agent import ChatAgent +from camel.logger import get_logger +from camel.models import ModelFactory +from camel.societies.workforce import Workforce +from camel.tasks.task import Task +from camel.toolkits import ( + AgentCommunicationToolkit, + HumanToolkit, +) +from camel.types import ModelPlatformType, ModelType + +logger = get_logger(__name__) + +WORKING_DIRECTORY = os.environ.get("CAMEL_WORKDIR") or os.path.abspath( + "working_dir/" +) + + +async def run_eigent_workforce(human_task: Task): + """ + Run the eigent workforce with a custom task. + + Args: + human_task (Task): The task to be processed by the workforce. + + Returns: + dict: A dictionary containing workforce KPIs and log information. + """ + # Ensure working directory exists + os.makedirs(WORKING_DIRECTORY, exist_ok=True) + + # Initialize the AgentCommunicationToolkit + msg_toolkit = AgentCommunicationToolkit(max_message_history=100) + + # Initialize message integration for use in coordinator and task agents + # message_integration = ToolkitMessageIntegration( + # message_handler=send_message_to_user + # ) + + # Create a single model backend for all agents + model_backend = ModelFactory.create( + model_platform=ModelPlatformType.MOONSHOT, + model_type=ModelType.MOONSHOT_KIMI_K2, + url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + model_config_dict={ + "stream": False, + }, + ) + # model_backend = ModelFactory.create( + # model_platform=ModelPlatformType.MOONSHOT, + # model_type=ModelType.MOONSHOT_KIMI_K2, + # url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + # model_config_dict={ + # "stream": False, + # }, + # ) + model_backend_reason = ModelFactory.create( + model_platform=ModelPlatformType.MOONSHOT, + model_type=ModelType.MOONSHOT_KIMI_K2, + url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + model_config_dict={ + "stream": False, + }, + ) + # model_backend_reason = ModelFactory.create( + # model_platform=ModelPlatformType.GEMINI, + # model_type=ModelType.GEMINI_2_5_PRO, + # # url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + # model_config_dict={ + # "stream": False, + # }, + # ) + + task_id = 'workforce_task' + + # Create custom agents for the workforce + coordinator_agent = ChatAgent( + system_message=( + f"""" +You are a helpful coordinator. +- You are now working in system {platform.system()} with architecture +{platform.machine()} at working directory `{WORKING_DIRECTORY}`. All local +file operations must occur here, but you can access files from any place in +the file system. For all file system operations, you MUST use absolute paths +to ensure precision and avoid ambiguity. +The current date is {datetime.date.today()}. For any date-related tasks, you +MUST use this as the current date. + +- If a task assigned to another agent fails, you should re-assign it to the +`Developer_Agent`. The `Developer_Agent` is a powerful agent with terminal +access and can resolve a wide range of issues. + """ + ), + enable_tool_output_cache=True, + model=model_backend_reason, + tools=[ + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), + ], + ) + task_agent = ChatAgent( + f""" + +You are a helpful task planner. +- You are now working in system {platform.system()} with architecture +{platform.machine()} at working directory `{WORKING_DIRECTORY}`. All local +file operations must occur here, but you can access files from any place in +the file system. For all file system operations, you MUST use absolute paths +to ensure precision and avoid ambiguity. +The current date is {datetime.date.today()}. For any date-related tasks, you +MUST use this as the current date. + """, + model=model_backend_reason, + tools=[ + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), + ], + ) + new_worker_agent = ChatAgent( + f"You are a helpful worker. When you complete your task, your " + "final response " + f"must be a comprehensive summary of your work, presented in a clear, " + f"detailed, and easy-to-read format. Avoid using markdown tables for " + f"presenting data; use plain text formatting instead. You are now " + f"working in " + f"`{WORKING_DIRECTORY}` All local file operations must occur here, " + f"but you can access files from any place in the file system. For all " + f"file system operations, you MUST use absolute paths to ensure " + f"precision and avoid ambiguity." + "directory. You can also communicate with other agents " + "using messaging tools - use `list_available_agents` to see " + "available team members and `send_message` to coordinate work " + "and ask for help when needed. " + "### Note-Taking: You have access to comprehensive note-taking tools " + "for documenting work progress and collaborating with team members. " + "Use create_note, append_note, read_note, and list_note to track " + "your work, share findings, and access information from other agents. " + "Create notes for work progress, discoveries, and collaboration " + "points.", + model=model_backend, + tools=[ + HumanToolkit().ask_human_via_console, + # *message_integration.register_toolkits( + # NoteTakingToolkit(working_directory=WORKING_DIRECTORY) + # ).get_tools(), + ], + ) + + # Create agents using factory functions + # search_agent_factory now returns (agent, browser_toolkit) tuple + search_agent, browser_toolkit = search_agent_factory( + model_backend, task_id + ) + + # document_agent = document_agent_factory( + # model_backend_reason, + # task_id, + # ) + # multi_modal_agent = multi_modal_agent_factory(model_backend, task_id) + + # Register all agents with the communication toolkit + msg_toolkit.register_agent("Worker", new_worker_agent) + msg_toolkit.register_agent("Search_Agent", search_agent) + # msg_toolkit.register_agent("Developer_Agent", developer_agent) + # msg_toolkit.register_agent("Document_Agent", document_agent) + # msg_toolkit.register_agent("Multi_Modal_Agent", multi_modal_agent) + + # Create workforce instance before adding workers + workforce = Workforce( + 'A workforce', + graceful_shutdown_timeout=30.0, # 30 seconds for debugging + share_memory=False, + coordinator_agent=coordinator_agent, + task_agent=task_agent, + new_worker_agent=new_worker_agent, + use_structured_output_handler=True, # Use handler for Moonshot compatibility + task_timeout_seconds=900.0, + ) + + workforce.add_single_agent_worker( + "Search Agent: An expert web researcher that can browse websites, " + "perform searches, and extract information to support other agents.", + worker=search_agent, + # ).add_single_agent_worker( + # "Developer Agent: A master-level coding assistant with a powerful " + # "terminal. It can write and execute code, manage files, automate " + # "desktop tasks, and deploy web applications to solve complex " + # "technical challenges.", + # worker=developer_agent, + # ).add_single_agent_worker( + # "Document Agent: A document processing assistant skilled in creating " + # "and modifying a wide range of file formats. It can generate " + # "text-based files (Markdown, JSON, YAML, HTML), office documents " + # "(Word, PDF), presentations (PowerPoint), and data files " + # "(Excel, CSV).", + # worker=document_agent, + ) + + try: + # Use the async version directly to avoid hanging with async tools + await workforce.process_task_async(human_task) + + # Test WorkforceLogger features + print("\n--- Workforce Log Tree ---") + print(workforce.get_workforce_log_tree()) + + print("\n--- Workforce KPIs ---") + kpis = workforce.get_workforce_kpis() + for key, value in kpis.items(): + print(f"{key}: {value}") + + log_file_path = f"eigent_logs_{human_task.id}.json" + print(f"\n--- Dumping Workforce Logs to {log_file_path} ---") + workforce.dump_workforce_logs(log_file_path) + print(f"Logs dumped. Please check the file: {log_file_path}") + + return { + "kpis": kpis, + "log_tree": workforce.get_workforce_log_tree(), + "log_file": log_file_path, + "browser_toolkit": browser_toolkit, # Return for cleanup + } + finally: + # Always close browser to avoid profile directory lock + if browser_toolkit: + try: + print("\n--- Closing browser to avoid profile lock ---") + await browser_toolkit.browser_close() + print("Browser closed successfully") + except Exception as e: + print(f"Warning: Failed to close browser: {e}") + + +async def run_loop(num_iterations: int = 3): + """ + Run the workforce multiple times in a loop. + This uses a single event loop to avoid hanging issues. + Browser is automatically closed after each iteration to avoid profile lock. + + Args: + human_task: The task to execute + num_iterations: Number of times to run (use a large number like 999999 for "infinite") + """ + try: + for i in range(num_iterations): + # names = ["Lordsorl M", "Lordsorl L", "Lordsorl K", "Lordsorl J", "Lordsorl I"] + # emails = ["Lordsorl.m@globalmedia.com", "Lordsorl.l@globalmedia.com", "Lordsorl.k@globalmedia.com", "Lordsorl.j@globalmedia.com", "Lordsorl.i@globalmedia.com"] + + # names = ["Mack M", "Mack L", "Mack K", "Mack J", "Mack I"] + # emails = ["Mack.m@globalmedia.com", "Mack.l@globalmedia.com", "Mack.k@globalmedia.com", "Mack.j@globalmedia.com", "Mack.i@globalmedia.com"] + # phones = ["(415) 566-0123", "(415) 566-0124", "(415) 566-0125", "(415) 566-0126", "(415) 566-0127"] + # addresses = ["229 Lordsorl Lane, London, England, SE22 8JF", "230 Lordsorl Lane, London, England, SE22 8JF", "231 Lordsorl Lane, London, England, SE22 8JF", "232 Lordsorl Lane, London, England, SE22 8JF", "233 Lordsorl Lane, London, England, SE22 8JF"] + human_task = Task( + content=( + """ + 不要拆分任务 + 要先用browser_open打开浏览器,browser_visit这个链接 + login https://energy-inspiration-9021.lightning.force.com/lightning/page/home + + user account weijie.bai-lqzb@force.com + password wodeSalesforce@1998 + +The salesforce.com - 200 Widgets deal is progressing well. Move it from 'Needs Analysis' to 'Proposal' stage, and click “Mark as Current Stage’ and go click "Contact Roles" and give me the contact name and Phone number. Back to Opportunities page edit this Next Step as “book a meeting with + the contact name and phone number.” + 如果type失败可能是因为ref发生了变化,你需要查看browser_get_page_snapshot + """ + ), + id='0', + ) + + print(f"\n{'='*60}") + print(f"Starting iteration {i+1}/{num_iterations}") + print(f"{'='*60}\n") + + result = await run_eigent_workforce(human_task) + + print(f"\n=== Iteration {i+1} Complete ===") + print(f"Log file saved to: {result['log_file']}") + + # Wait for browser process to fully terminate before next iteration + if i < num_iterations - 1: # Don't wait after last iteration + print("Waiting 2 seconds for browser cleanup...") + await asyncio.sleep(2) + + except KeyboardInterrupt: + print("\n\n⚠️ Loop interrupted by user (Ctrl+C)") + print("Exiting gracefully...") + except Exception as e: + print(f"\n\n❌ Error occurred: {e}") + raise + + +# Example usage +if __name__ == "__main__": + # Create a custom task + + # Run the workforce in a loop (press Ctrl+C to stop) + # Uses a single event loop to avoid hanging + asyncio.run(run_loop(num_iterations=5)) diff --git a/examples/workforce/run_eigent_webvoyage.py b/examples/workforce/run_eigent_webvoyage.py new file mode 100644 index 0000000000..68326f4416 --- /dev/null +++ b/examples/workforce/run_eigent_webvoyage.py @@ -0,0 +1,554 @@ +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= + +import asyncio +import datetime +import json +import os +import platform + +# Import agent factories from eigent.py +from eigent import ( + # developer_agent_factory, + # document_agent_factory, + # multi_modal_agent_factory, + search_agent_factory, + send_message_to_user, +) + +from camel.agents.chat_agent import ChatAgent +from camel.logger import get_logger +from camel.messages.base import BaseMessage +from camel.models import BaseModelBackend, ModelFactory +from camel.societies.workforce import Workforce +from camel.tasks.task import Task +from camel.toolkits import ( + AgentCommunicationToolkit, + HumanToolkit, + # NoteTakingToolkit, + ToolkitMessageIntegration, +) +from camel.types import ModelPlatformType, ModelType + +logger = get_logger(__name__) + +WORKING_DIRECTORY = os.environ.get("CAMEL_WORKDIR") or os.path.abspath( + "working_dir/" +) + + +def load_tasks_from_jsonl( + jsonl_path: str, start_index: int = 0, end_index: int = None +): + """ + Load tasks from a JSONL file. + + Args: + jsonl_path (str): Path to the JSONL file. + start_index (int): Starting index (inclusive). + end_index (int): Ending index (inclusive). If None, load all tasks from start_index. + + Returns: + list: A list of task dictionaries. + """ + tasks = [] + with open(jsonl_path, 'r', encoding='utf-8') as f: + for i, line in enumerate(f): + if i < start_index: + continue + if end_index is not None and i > end_index: + break + tasks.append(json.loads(line.strip())) + return tasks + + +async def verify_result_with_agent( + task_content: str, result: dict, model_backend: BaseModelBackend +): + """ + Verify the workforce result using a chat agent. + + Args: + task_content (str): The original task content. + result (dict): The result from the workforce. + model_backend (BaseModelBackend): The model backend to use. + + Returns: + dict: A dictionary with 'success' (bool) and 'reasoning' (str). + """ + verifier_agent = ChatAgent( + system_message=( + "You are a task verification expert. Your job is to analyze whether " + "a task was completed successfully based on the task description and " + "the execution result. You should output your verification in JSON format " + "with two fields: 'success' (true/false) and 'reasoning' (explanation)." + ), + model=model_backend, + ) + + # Read the detailed log file for verification + detailed_logs = None + final_response = None + log_file_path = result.get('log_file') + if log_file_path and os.path.exists(log_file_path): + try: + with open(log_file_path, 'r', encoding='utf-8') as f: + detailed_logs = json.load(f) + + # Extract the final response from logs + # Look for the last worker response or task completion + if isinstance(detailed_logs, dict): + # Try to find the final agent response + if 'logs' in detailed_logs: + logs_list = detailed_logs['logs'] + # Find the last message from a worker + for log_entry in reversed(logs_list): + if ( + isinstance(log_entry, dict) + and 'content' in log_entry + ): + final_response = log_entry.get('content') + break + elif 'final_result' in detailed_logs: + final_response = detailed_logs['final_result'] + + except Exception as e: + logger.warning(f"Failed to read log file {log_file_path}: {e}") + + # Build verification prompt with all available information + verification_prompt_parts = [ + "Task Description:", + task_content, + "", + "Execution Summary (KPIs):", + json.dumps(result['kpis'], indent=2), + "", + ] + + # Add final response if extracted + if final_response: + verification_prompt_parts.extend( + [ + "Final Agent Response:", + str(final_response), + "", + ] + ) + + # Add condensed log information if available + if detailed_logs: + # Limit the size of logs to avoid token overflow + logs_str = json.dumps(detailed_logs, indent=2, ensure_ascii=False) + max_log_length = 10000 # Limit to ~10k characters + if len(logs_str) > max_log_length: + logs_str = logs_str[:max_log_length] + "\n... (truncated)" + + verification_prompt_parts.extend( + [ + "Detailed Execution Logs (summary):", + logs_str, + "", + ] + ) + + verification_prompt_parts.extend( + [ + "Please verify if the task was completed successfully. Consider:", + "1. Did the workforce complete the task objectives?", + "2. Are there any errors or failures in the execution?", + "3. Does the final result/answer align with the task requirements?", + "4. Did the agent provide a comprehensive and accurate response?", + "", + "Output your verification in JSON format:", + "{", + ' "success": true/false,', + ' "reasoning": "your detailed explanation here"', + "}", + ] + ) + + verification_prompt = "\n".join(verification_prompt_parts) + + response = verifier_agent.step( + BaseMessage.make_user_message( + role_name="Verifier", content=verification_prompt + ) + ) + + try: + # Try to parse JSON from the response + content = response.msg.content + # Find JSON in the content + start_idx = content.find('{') + end_idx = content.rfind('}') + 1 + if start_idx != -1 and end_idx > start_idx: + json_str = content[start_idx:end_idx] + verification_result = json.loads(json_str) + else: + # Fallback if no JSON found + verification_result = { + "success": False, + "reasoning": "Failed to parse verification response", + } + except Exception as e: + verification_result = { + "success": False, + "reasoning": f"Error parsing verification: {e!s}", + } + + return verification_result + + +async def run_eigent_workforce(human_task: Task, task_index: int = None): + """ + Run the eigent workforce with a custom task. + + Args: + human_task (Task): The task to be processed by the workforce. + task_index (int): Optional task index for unique log file naming. + + Returns: + dict: A dictionary containing workforce KPIs and log information. + """ + # Ensure working directory exists + os.makedirs(WORKING_DIRECTORY, exist_ok=True) + + # Initialize the AgentCommunicationToolkit + msg_toolkit = AgentCommunicationToolkit(max_message_history=100) + + # Initialize message integration for use in coordinator and task agents + message_integration = ToolkitMessageIntegration( + message_handler=send_message_to_user + ) + + # Create a single model backend for all agents + model_backend = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4_1, + model_config_dict={ + "stream": False, + }, + ) + + model_backend_reason = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4_1, + model_config_dict={ + "stream": False, + }, + ) + + task_id = 'workforce_task' + + # Create custom agents for the workforce + coordinator_agent = ChatAgent( + system_message=( + f"""" +You are a helpful coordinator. +- You are now working in system {platform.system()} with architecture +{platform.machine()} at working directory `{WORKING_DIRECTORY}`. All local +file operations must occur here, but you can access files from any place in +the file system. For all file system operations, you MUST use absolute paths +to ensure precision and avoid ambiguity. +The current date is {datetime.date.today()}. For any date-related tasks, you +MUST use this as the current date. + +- If a task assigned to another agent fails, you should re-assign it to the +`Developer_Agent`. The `Developer_Agent` is a powerful agent with terminal +access and can resolve a wide range of issues. + """ + ), + enable_tool_output_cache=True, + model=model_backend_reason, + tools=[ + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), + ], + ) + task_agent = ChatAgent( + f""" + +You are a helpful task planner. +- You are now working in system {platform.system()} with architecture +{platform.machine()} at working directory `{WORKING_DIRECTORY}`. All local +file operations must occur here, but you can access files from any place in +the file system. For all file system operations, you MUST use absolute paths +to ensure precision and avoid ambiguity. +The current date is {datetime.date.today()}. For any date-related tasks, you +MUST use this as the current date. + """, + enable_tool_output_cache=True, + model=model_backend_reason, + tools=[ + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), + ], + ) + new_worker_agent = ChatAgent( + f"You are a helpful worker. When you complete your task, your " + "final response " + f"must be a comprehensive summary of your work, presented in a clear, " + f"detailed, and easy-to-read format. Avoid using markdown tables for " + f"presenting data; use plain text formatting instead. You are now " + f"working in " + f"`{WORKING_DIRECTORY}` All local file operations must occur here, " + f"but you can access files from any place in the file system. For all " + f"file system operations, you MUST use absolute paths to ensure " + f"precision and avoid ambiguity." + "directory. You can also communicate with other agents " + "using messaging tools - use `list_available_agents` to see " + "available team members and `send_message` to coordinate work " + "and ask for help when needed. " + "### Note-Taking: You have access to comprehensive note-taking tools " + "for documenting work progress and collaborating with team members. " + "Use create_note, append_note, read_note, and list_note to track " + "your work, share findings, and access information from other agents. " + "Create notes for work progress, discoveries, and collaboration " + "points.", + model=model_backend, + tools=[ + HumanToolkit().ask_human_via_console, + # *message_integration.register_toolkits( + # NoteTakingToolkit(working_directory=WORKING_DIRECTORY) + # ).get_tools(), + ], + ) + + # Create agents using factory functions + # search_agent_factory now returns (agent, browser_toolkit) + search_agent, browser_toolkit = search_agent_factory( + model_backend, task_id + ) + + # document_agent = document_agent_factory( + # model_backend_reason, + # task_id, + # ) + # multi_modal_agent = multi_modal_agent_factory(model_backend, task_id) + + # Register all agents with the communication toolkit + msg_toolkit.register_agent("Worker", new_worker_agent) + msg_toolkit.register_agent("Search_Agent", search_agent) + # msg_toolkit.register_agent("Developer_Agent", developer_agent) + # msg_toolkit.register_agent("Document_Agent", document_agent) + # msg_toolkit.register_agent("Multi_Modal_Agent", multi_modal_agent) + + # Create workforce instance before adding workers + workforce = Workforce( + 'A workforce', + graceful_shutdown_timeout=30.0, # 30 seconds for debugging + share_memory=False, + coordinator_agent=coordinator_agent, + task_agent=task_agent, + new_worker_agent=new_worker_agent, + use_structured_output_handler=False, + task_timeout_seconds=900.0, + ) + + workforce.add_single_agent_worker( + "Search Agent: An expert web researcher that can browse websites, " + "perform searches, and extract information to support other agents.", + worker=search_agent, + # ).add_single_agent_worker( + # "Developer Agent: A master-level coding assistant with a powerful " + # "terminal. It can write and execute code, manage files, automate " + # "desktop tasks, and deploy web applications to solve complex " + # "technical challenges.", + # worker=developer_agent, + # ).add_single_agent_worker( + # "Document Agent: A document processing assistant skilled in creating " + # "and modifying a wide range of file formats. It can generate " + # "text-based files (Markdown, JSON, YAML, HTML), office documents " + # "(Word, PDF), presentations (PowerPoint), and data files " + # "(Excel, CSV).", + # worker=document_agent, + ) + + try: + # Use the async version directly to avoid hanging with async tools + await workforce.process_task_async(human_task) + + # Test WorkforceLogger features + print("\n--- Workforce Log Tree ---") + print(workforce.get_workforce_log_tree()) + + print("\n--- Workforce KPIs ---") + kpis = workforce.get_workforce_kpis() + for key, value in kpis.items(): + print(f"{key}: {value}") + + # Use task_index for unique log file naming if provided + if task_index is not None: + log_file_path = ( + f"eigent_logs_task_{task_index}_{human_task.id}.json" + ) + else: + log_file_path = f"eigent_logs_{human_task.id}.json" + + print(f"\n--- Dumping Workforce Logs to {log_file_path} ---") + workforce.dump_workforce_logs(log_file_path) + print(f"Logs dumped. Please check the file: {log_file_path}") + + return { + "kpis": kpis, + "log_tree": workforce.get_workforce_log_tree(), + "log_file": log_file_path, + } + finally: + # IMPORTANT: Close browser after each task to prevent resource leaks + if browser_toolkit is not None: + try: + print(f"\n--- Closing Browser for Task {human_task.id} ---") + await browser_toolkit.browser_close() + print("Browser closed successfully.") + except Exception as e: + print(f"Error closing browser: {e}") + + +# Example usage +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description='Run eigent workforce on WebVoyager tasks' + ) + parser.add_argument( + '--start', type=int, default=0, help='Start index (inclusive)' + ) + parser.add_argument( + '--end', type=int, default=None, help='End index (inclusive)' + ) + parser.add_argument( + '--jsonl', + type=str, + default='/Users/puzhen/Downloads/WebVoyager_data.jsonl', + help='Path to JSONL file', + ) + args = parser.parse_args() + + # Load tasks from JSONL file + tasks = load_tasks_from_jsonl(args.jsonl, args.start, args.end) + print( + f"Loaded {len(tasks)} tasks (index {args.start} to {args.end if args.end else 'end'})" + ) + + # Create model backend for verification + verifier_model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4_1, + model_config_dict={ + "stream": False, + }, + ) + + # Store all results + all_results = [] + + async def process_all_tasks(): + for idx, task_data in enumerate(tasks): + actual_idx = args.start + idx + print(f"\n{'='*80}") + print( + f"Processing Task {actual_idx}: {task_data.get('id', 'unknown')}" + ) + print(f"{'='*80}") + + # Combine ques and web fields + task_content = f"{task_data['ques']} in \"{task_data['web']}\"" + + # Create a custom task + human_task = Task( + content=task_content, + id=task_data.get('id', str(actual_idx)), + ) + + try: + # Run the workforce with task index for unique log files + result = await run_eigent_workforce( + human_task, task_index=actual_idx + ) + + print("\n--- Verifying Result with Agent ---") + # Verify the result + verification = await verify_result_with_agent( + task_content, result, verifier_model + ) + + print("\nVerification Result:") + print(f" Success: {verification['success']}") + print(f" Reasoning: {verification['reasoning']}") + + # Store complete result + task_result = { + "task_index": actual_idx, + "task_id": task_data.get('id', str(actual_idx)), + "task_content": task_content, + "workforce_result": result, + "verification": verification, + } + all_results.append(task_result) + + # Save individual task result + individual_result_file = f"task_result_{actual_idx}_{task_data.get('id', str(actual_idx))}.json" + with open(individual_result_file, 'w', encoding='utf-8') as f: + json.dump(task_result, f, indent=2, ensure_ascii=False) + + # Save accumulated results (all tasks so far) + results_file = f"all_results_{args.start}_{args.end if args.end else 'end'}.json" + with open(results_file, 'w', encoding='utf-8') as f: + json.dump(all_results, f, indent=2, ensure_ascii=False) + + print(f"\n=== Task {actual_idx} Complete ===") + print(f"Workforce log: {result['log_file']}") + print(f"Task result: {individual_result_file}") + print(f"All results: {results_file}") + + except Exception as e: + print(f"\n!!! Error processing task {actual_idx}: {e!s}") + error_result = { + "task_index": actual_idx, + "task_id": task_data.get('id', str(actual_idx)), + "task_content": task_content, + "error": str(e), + "verification": { + "success": False, + "reasoning": f"Exception occurred: {e!s}", + }, + } + all_results.append(error_result) + + # Save individual error result + individual_result_file = f"task_result_{actual_idx}_{task_data.get('id', str(actual_idx))}.json" + with open(individual_result_file, 'w', encoding='utf-8') as f: + json.dump(error_result, f, indent=2, ensure_ascii=False) + + # Save accumulated results + results_file = f"all_results_{args.start}_{args.end if args.end else 'end'}.json" + with open(results_file, 'w', encoding='utf-8') as f: + json.dump(all_results, f, indent=2, ensure_ascii=False) + + print(f"Error result saved to: {individual_result_file}") + + # Run all tasks + asyncio.run(process_all_tasks()) + + print(f"\n{'='*80}") + print("=== All Tasks Complete ===") + print(f"{'='*80}") + print(f"Processed {len(tasks)} tasks") + print( + f"Results saved to: all_results_{args.start}_{args.end if args.end else 'end'}.json" + )