From 724f4385d6c39118ad6d9a7e64bc9d1fe7640ed0 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 13 Oct 2025 18:49:47 +0100 Subject: [PATCH 01/13] update --- camel/models/base_model.py | 2 +- .../hybrid_browser_toolkit_example.py | 28 ++++---- examples/workforce/eigent.py | 69 +++++-------------- 3 files changed, 32 insertions(+), 67 deletions(-) diff --git a/camel/models/base_model.py b/camel/models/base_model.py index 023f543a1e..297e62c5d8 100644 --- a/camel/models/base_model.py +++ b/camel/models/base_model.py @@ -117,7 +117,7 @@ def __init__( self._max_retries = max_retries # Initialize logging configuration self._log_enabled = ( - os.environ.get("CAMEL_MODEL_LOG_ENABLED", "False").lower() + os.environ.get("CAMEL_MODEL_LOG_ENABLED", "True").lower() == "true" ) self._log_dir = os.environ.get("CAMEL_LOG_DIR", "camel_logs") diff --git a/examples/toolkits/hybrid_browser_toolkit_example.py b/examples/toolkits/hybrid_browser_toolkit_example.py index 28b7a3d62e..b356880603 100644 --- a/examples/toolkits/hybrid_browser_toolkit_example.py +++ b/examples/toolkits/hybrid_browser_toolkit_example.py @@ -33,7 +33,7 @@ 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, @@ -105,18 +105,20 @@ async def main() -> None: - try: - response = await agent.astep(TASK_PROMPT) - print("Task:", TASK_PROMPT) - print(f"Using user data directory: {USER_DATA_DIR}") - print(f"Enabled tools: {web_toolkit_custom.enabled_tools}") - print("\nResponse from agent:") - print(response.msgs[0].content if response.msgs else "") - finally: - # Ensure browser is closed properly - print("\nClosing browser...") - await web_toolkit_custom.browser_close() - print("Browser closed successfully.") + await web_toolkit_custom.browser_open() + # await web_toolkit_custom.browser_close() + # try: + # response = await agent.astep(TASK_PROMPT) + # print("Task:", TASK_PROMPT) + # print(f"Using user data directory: {USER_DATA_DIR}") + # print(f"Enabled tools: {web_toolkit_custom.enabled_tools}") + # print("\nResponse from agent:") + # print(response.msgs[0].content if response.msgs else "") + # finally: + # # Ensure browser is closed properly + # print("\nClosing browser...") + # await web_toolkit_custom.browser_close() + # print("Browser closed successfully.") if __name__ == "__main__": diff --git a/examples/workforce/eigent.py b/examples/workforce/eigent.py index 4b749d443e..ff9aa25dcf 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -289,13 +289,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, @@ -346,7 +346,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,7 +356,7 @@ def search_agent_factory( *enhanced_shell_exec, HumanToolkit().ask_human_via_console, *note_toolkit.get_tools(), - *search_toolkit, + # *search_toolkit, *terminal_toolkit.get_tools(), ] @@ -368,25 +368,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 +383,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 +401,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,16 +414,12 @@ 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. """ @@ -490,7 +453,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 +466,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 +476,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 +617,7 @@ def document_agent_factory( role_name="Document Agent", content=system_message, ), + enable_tool_output_cache=True, model=model, tools=tools, ) @@ -1097,8 +1061,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', From 99d99fb3a1d024e78d4baa8ed4685e61ae9ecfcc Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 13 Oct 2025 23:37:13 +0100 Subject: [PATCH 02/13] update --- examples/workforce/eigent.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/workforce/eigent.py b/examples/workforce/eigent.py index ff9aa25dcf..3f0116142b 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -323,6 +323,8 @@ def search_agent_factory( "browser_visit_page", "browser_get_som_screenshot", ] + 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 @@ -429,6 +433,7 @@ def search_agent_factory( 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, From 18f1d223df23f78378d3dee7c97a5d2bba01da5b Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 13 Oct 2025 23:40:13 +0100 Subject: [PATCH 03/13] update --- examples/workforce/run_eigent.py | 273 +++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 examples/workforce/run_eigent.py diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py new file mode 100644 index 0000000000..38b8e61908 --- /dev/null +++ b/examples/workforce/run_eigent.py @@ -0,0 +1,273 @@ +# ========= 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 uuid + +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 + +# 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, +) + +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.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. + """ + ), + 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 = 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, + ) + + # 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 + } + + +# Example usage +if __name__ == "__main__": + # Create a custom task + human_task = Task( + content=( + """ + 不要拆分任务!所有任务由一个search agent完成 + + 用浏览器打开https://my426318.s4hana.cloud.sap/ui?sap-language=EN&help-mixedLanguages=false&help-autoStartTour=PR_28A0E50CC77DAF91#Shell-home,这是sap平台,帮我执行下面的任务: + 点击 'Procurement' + 然后 + 点击 'Manage Purchase Orders' + you click 'create' button + 在'Purchasing Doc. Type:'中 + 选中输入Standard PO (NB) + 在'Currency'输入 + United States Dollar (USD) + 在'Purchasing Group:'中 + 输入Group 001 (001) + 在 'Company Code:'中输入 + Velotics Inc. (1710) + 在 'Purchasing Organization:'输入 + Purch. Org. 1710 (1710) + + Supplier是AdvertOne (USSU-TRL03) +然后在items表中点击add from document, 点击后在两个purchase order checkbox中点击第一个,然后点击add items确认 + 然后点击order即可 +注意:如果遇到leave tour按钮,那么先点击这个按钮 + """ + ), + id='0', + ) + + # Run the workforce + result = asyncio.run(run_eigent_workforce(human_task)) + + print("\n=== Execution Complete ===") + print(f"Log file saved to: {result['log_file']}") From 7ef4e38bcd166ee6d7e3a5d14a3e58abfd778433 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Mon, 13 Oct 2025 23:59:50 +0100 Subject: [PATCH 04/13] update --- examples/workforce/run_eigent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py index 38b8e61908..f63ea5a689 100644 --- a/examples/workforce/run_eigent.py +++ b/examples/workforce/run_eigent.py @@ -258,7 +258,7 @@ async def run_eigent_workforce(human_task: Task): Purch. Org. 1710 (1710) Supplier是AdvertOne (USSU-TRL03) -然后在items表中点击add from document, 点击后在两个purchase order checkbox中点击第一个,然后点击add items确认 +然后在items表中点击add from document, 点击Go button 后, 在purchase order checkbox中点击第一个,然后点击add items确认 然后点击order即可 注意:如果遇到leave tour按钮,那么先点击这个按钮 """ From 82b572cacd883a4cabecbaa9186f1da1d65ece66 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 14 Oct 2025 00:38:06 +0100 Subject: [PATCH 05/13] update --- examples/workforce/eigent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/workforce/eigent.py b/examples/workforce/eigent.py index 3f0116142b..9c4757bf65 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -322,6 +322,7 @@ def search_agent_factory( "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" @@ -436,7 +437,7 @@ def search_agent_factory( 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, ) From 9e1a3cf754096c1c2e78ddd197d6caecd0492841 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 14 Oct 2025 01:23:31 +0100 Subject: [PATCH 06/13] update --- .../hybrid_browser_toolkit_ts.py | 47 +++++++++---- .../ts/src/browser-session.ts | 69 ++++++++++++++----- examples/workforce/run_eigent.py | 1 + 3 files changed, 89 insertions(+), 28 deletions(-) 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..c62c0eecdc 100644 --- a/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +++ b/camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py @@ -733,26 +733,49 @@ async def browser_type( 2. Multiple inputs mode: Provide 'inputs' as a list of dictionaries with 'ref' and 'text' keys + The multiple inputs mode is more efficient for filling forms as it + batches all typing operations into a single browser action, reducing + overhead and improving performance. + Args: ref (Optional[str]): The `ref` ID of the input element, from a - snapshot. Required when using single input mode. + snapshot. Required when using single input mode. Mutually + exclusive with `inputs`. 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'}] + when using single input mode. Mutually exclusive with `inputs`. + inputs (Optional[List[Dict[str, str]]]): List of dictionaries for + typing into multiple elements. Each dictionary must contain: + - 'ref' (str): The element reference ID + - 'text' (str): The text to type into that element + Mutually exclusive with `ref` and `text`. 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). + + Raises: + ValueError: If neither mode's parameters are provided, or if both + modes' parameters are provided simultaneously. + + Examples: + Single input mode: + >>> browser_type( + ... ref="3", + ... text="hello@example.com" + ... ) + Typed text into element 3 + + Multiple inputs mode: + >>> browser_type([ + ... {"ref": "5", "text": "Standard PO (NB)"}, + ... {"ref": "7", "text": "Group 001 (001)"}, + ... {"ref": "9", "text": "Velotics Inc. (1710)"} + ... ]) + """ try: ws_wrapper = await self._get_ws_wrapper() 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/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py index f63ea5a689..43ee937ca7 100644 --- a/examples/workforce/run_eigent.py +++ b/examples/workforce/run_eigent.py @@ -261,6 +261,7 @@ async def run_eigent_workforce(human_task: Task): 然后在items表中点击add from document, 点击Go button 后, 在purchase order checkbox中点击第一个,然后点击add items确认 然后点击order即可 注意:如果遇到leave tour按钮,那么先点击这个按钮 +注意:如果多个输入框在同一个页面你都看到了,那么就用browser_type([{ref:...,text:...},{ref,text}...])这样一次输入多个输入框 """ ), id='0', From cf5df7fa59e68409b456b53dc8390921410f80eb Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Tue, 14 Oct 2025 15:41:42 +0100 Subject: [PATCH 07/13] update --- .../ts/websocket-server.js | 3 +- examples/workforce/eigent.py | 1 + examples/workforce/run_eigent.py | 32 ++++++------------- 3 files changed, 12 insertions(+), 24 deletions(-) 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/examples/workforce/eigent.py b/examples/workforce/eigent.py index 9c4757bf65..72a1829c4d 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -313,6 +313,7 @@ def search_agent_factory( custom_tools = [ "browser_open", + "browser_select", "browser_close", "browser_back", "browser_forward", diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py index 43ee937ca7..27e97aaf92 100644 --- a/examples/workforce/run_eigent.py +++ b/examples/workforce/run_eigent.py @@ -239,29 +239,15 @@ async def run_eigent_workforce(human_task: Task): human_task = Task( content=( """ - 不要拆分任务!所有任务由一个search agent完成 - - 用浏览器打开https://my426318.s4hana.cloud.sap/ui?sap-language=EN&help-mixedLanguages=false&help-autoStartTour=PR_28A0E50CC77DAF91#Shell-home,这是sap平台,帮我执行下面的任务: - 点击 'Procurement' - 然后 - 点击 'Manage Purchase Orders' - you click 'create' button - 在'Purchasing Doc. Type:'中 - 选中输入Standard PO (NB) - 在'Currency'输入 - United States Dollar (USD) - 在'Purchasing Group:'中 - 输入Group 001 (001) - 在 'Company Code:'中输入 - Velotics Inc. (1710) - 在 'Purchasing Organization:'输入 - Purch. Org. 1710 (1710) - - Supplier是AdvertOne (USSU-TRL03) -然后在items表中点击add from document, 点击Go button 后, 在purchase order checkbox中点击第一个,然后点击add items确认 - 然后点击order即可 -注意:如果遇到leave tour按钮,那么先点击这个按钮 -注意:如果多个输入框在同一个页面你都看到了,那么就用browser_type([{ref:...,text:...},{ref,text}...])这样一次输入多个输入框 +login https://energy-inspiration-9021.lightning.force.com/lightning/page/home + +user account weijie.bai-lqzb@force.com +password wodeSalesforce@1998 + +Please create a new lead in Salesforce for me, We met her at an Amazon event. Her name is Ms.Celine Xie, and she's the Head of Marketing at Eigent UK Ltd. The company is in the technology industry, has around 25 employees, and their address is 344-354 Gray's Inn Road, WC1X 8BP. Her phone number is +44 07962637373. + +Please make sure the lead status is set to 'New'. +注意不要忘记写地址, 如果type失败可能是因为ref发生了变化,你需要查看browser_get_page_snapshot """ ), id='0', From c0cc6b50acdbba52301aaf047f0949717e7f9c5e Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 15 Oct 2025 18:36:09 +0100 Subject: [PATCH 08/13] update --- examples/workforce/run_eigent.py | 56 ++- examples/workforce/run_eigent_webvoyage.py | 425 +++++++++++++++++++++ 2 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 examples/workforce/run_eigent_webvoyage.py diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py index 27e97aaf92..77ed146418 100644 --- a/examples/workforce/run_eigent.py +++ b/examples/workforce/run_eigent.py @@ -27,7 +27,7 @@ from camel.toolkits import ( AgentCommunicationToolkit, HumanToolkit, - NoteTakingToolkit, + # NoteTakingToolkit, ToolkitMessageIntegration, ) from camel.types import ModelPlatformType, ModelType @@ -65,26 +65,43 @@ async def run_eigent_workforce(human_task: Task): 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 - ) + # 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, + # 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.OPENAI, model_type=ModelType.GPT_4_1, + # 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' @@ -106,11 +123,12 @@ async def run_eigent_workforce(human_task: Task): 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(), + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), ], ) task_agent = ChatAgent( @@ -127,9 +145,9 @@ async def run_eigent_workforce(human_task: Task): """, model=model_backend_reason, tools=[ - *NoteTakingToolkit( - working_directory=WORKING_DIRECTORY - ).get_tools(), + # *NoteTakingToolkit( + # working_directory=WORKING_DIRECTORY + # ).get_tools(), ], ) new_worker_agent = ChatAgent( @@ -156,9 +174,9 @@ async def run_eigent_workforce(human_task: Task): model=model_backend, tools=[ HumanToolkit().ask_human_via_console, - *message_integration.register_toolkits( - NoteTakingToolkit(working_directory=WORKING_DIRECTORY) - ).get_tools(), + # *message_integration.register_toolkits( + # NoteTakingToolkit(working_directory=WORKING_DIRECTORY) + # ).get_tools(), ], ) @@ -186,7 +204,7 @@ async def run_eigent_workforce(human_task: Task): coordinator_agent=coordinator_agent, task_agent=task_agent, new_worker_agent=new_worker_agent, - use_structured_output_handler=False, + use_structured_output_handler=True, # Use handler for Moonshot compatibility task_timeout_seconds=900.0, ) @@ -239,15 +257,17 @@ async def run_eigent_workforce(human_task: Task): 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 -Please create a new lead in Salesforce for me, We met her at an Amazon event. Her name is Ms.Celine Xie, and she's the Head of Marketing at Eigent UK Ltd. The company is in the technology industry, has around 25 employees, and their address is 344-354 Gray's Inn Road, WC1X 8BP. Her phone number is +44 07962637373. +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 home page edit this Next Step as “book a meeting with + the contact name and phone number.” + -Please make sure the lead status is set to 'New'. -注意不要忘记写地址, 如果type失败可能是因为ref发生了变化,你需要查看browser_get_page_snapshot +如果type失败可能是因为ref发生了变化,你需要查看browser_get_page_snapshot """ ), id='0', diff --git a/examples/workforce/run_eigent_webvoyage.py b/examples/workforce/run_eigent_webvoyage.py new file mode 100644 index 0000000000..121f7f14cc --- /dev/null +++ b/examples/workforce/run_eigent_webvoyage.py @@ -0,0 +1,425 @@ +# ========= 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 uuid + +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 + +# 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, +) + +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, + ) + + verification_prompt = f""" +Task Description: +{task_content} + +Execution Result: +{json.dumps(result, indent=2)} + +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 logs? +3. Does the result align with the task requirements? + +Output your verification in JSON format: +{{ + "success": true/false, + "reasoning": "your explanation here" +}} +""" + + 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: {str(e)}" + } + + return verification_result + + +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.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. + """ + ), + 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 = 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, + ) + + # 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 + } + + +# 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 + result = await run_eigent_workforce(human_task) + + print("\n--- Verifying Result with Agent ---") + # Verify the result + verification = await verify_result_with_agent(task_content, result, verifier_model) + + print(f"\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 intermediate 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"\n=== Task {actual_idx} Complete ===") + print(f"Log file saved to: {result['log_file']}") + print(f"Results saved to: {results_file}") + + except Exception as e: + print(f"\n!!! Error processing task {actual_idx}: {str(e)}") + 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: {str(e)}" + } + } + all_results.append(error_result) + + # 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") From f1667e44fda992e19d5fd5aa1435b7ad91f7cd78 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 15 Oct 2025 19:28:20 +0100 Subject: [PATCH 09/13] update --- camel/models/moonshot_model.py | 61 ++++++- .../hybrid_browser_toolkit_ts.py | 69 ++------ camel/types/enums.py | 2 +- examples/workforce/eigent.py | 9 +- examples/workforce/run_eigent_webvoyage.py | 154 +++++++++++++----- 5 files changed, 195 insertions(+), 100 deletions(-) diff --git a/camel/models/moonshot_model.py b/camel/models/moonshot_model.py index ba27e7f47b..f001c51bff 100644 --- a/camel/models/moonshot_model.py +++ b/camel/models/moonshot_model.py @@ -130,7 +130,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 +140,63 @@ 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): + # Handle anyOf with null types + if 'anyOf' in schema: + any_of = schema['anyOf'] + # Filter out null types + filtered = [ + item for item in any_of + if not (isinstance(item, dict) and item.get('type') == 'null') + ] + # If only one type remains, simplify + if len(filtered) == 1: + return remove_null_from_schema(filtered[0]) + elif len(filtered) > 1: + schema = schema.copy() + schema['anyOf'] = [remove_null_from_schema(item) for item in filtered] + return schema + else: + # All were null, return string type as fallback + return {"type": "string"} + + # Recursively process nested dicts + return { + k: remove_null_from_schema(v) + for k, v in schema.items() + } + 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 c62c0eecdc..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,32 +721,14 @@ 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 - - The multiple inputs mode is more efficient for filling forms as it - batches all typing operations into a single browser action, reducing - overhead and improving performance. + 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. Mutually - exclusive with `inputs`. - text (Optional[str]): The text to type into the element. Required - when using single input mode. Mutually exclusive with `inputs`. - inputs (Optional[List[Dict[str, str]]]): List of dictionaries for - typing into multiple elements. Each dictionary must contain: - - 'ref' (str): The element reference ID - - 'text' (str): The text to type into that element - Mutually exclusive with `ref` and `text`. + 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: @@ -757,38 +738,16 @@ async def browser_type( - "tabs" (List[Dict]): Information about all open tabs. - "current_tab" (int): Index of the active tab (zero-based). - Raises: - ValueError: If neither mode's parameters are provided, or if both - modes' parameters are provided simultaneously. - - Examples: - Single input mode: - >>> browser_type( - ... ref="3", - ... text="hello@example.com" - ... ) - Typed text into element 3 - - Multiple inputs mode: - >>> browser_type([ - ... {"ref": "5", "text": "Standard PO (NB)"}, - ... {"ref": "7", "text": "Group 001 (001)"}, - ... {"ref": "9", "text": "Velotics Inc. (1710)"} - ... ]) - + 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/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/workforce/eigent.py b/examples/workforce/eigent.py index 72a1829c4d..942c54f884 100644 --- a/examples/workforce/eigent.py +++ b/examples/workforce/eigent.py @@ -322,7 +322,6 @@ 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" @@ -429,7 +428,7 @@ def search_agent_factory( """ - return ChatAgent( + agent = ChatAgent( system_message=BaseMessage.make_assistant_message( role_name="Search Agent", content=system_message, @@ -441,6 +440,9 @@ def search_agent_factory( # 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, @@ -987,7 +989,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, diff --git a/examples/workforce/run_eigent_webvoyage.py b/examples/workforce/run_eigent_webvoyage.py index 121f7f14cc..a59c67cc6e 100644 --- a/examples/workforce/run_eigent_webvoyage.py +++ b/examples/workforce/run_eigent_webvoyage.py @@ -94,24 +94,79 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen model=model_backend, ) - verification_prompt = f""" -Task Description: -{task_content} - -Execution Result: -{json.dumps(result, indent=2)} - -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 logs? -3. Does the result align with the task requirements? - -Output your verification in JSON format: -{{ - "success": true/false, - "reasoning": "your explanation here" -}} -""" + # 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", @@ -142,12 +197,13 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen return verification_result -async def run_eigent_workforce(human_task: Task): +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. @@ -200,6 +256,8 @@ async def run_eigent_workforce(human_task: Task): access and can resolve a wide range of issues. """ ), + + enable_tool_output_cache=True, model=model_backend_reason, tools=[ # *NoteTakingToolkit( @@ -257,7 +315,8 @@ async def run_eigent_workforce(human_task: Task): ) # Create agents using factory functions - search_agent = search_agent_factory(model_backend, task_id) + # 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, @@ -303,28 +362,43 @@ async def run_eigent_workforce(human_task: Task): # worker=document_agent, ) - # Use the async version directly to avoid hanging with async tools - await workforce.process_task_async(human_task) + 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()) - # 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}") - 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" - 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}") + 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 - } + 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 @@ -371,8 +445,8 @@ async def process_all_tasks(): ) try: - # Run the workforce - result = await run_eigent_workforce(human_task) + # 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 From 5a978b95b9903c368973fc775320123fdd18d0ad Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Wed, 15 Oct 2025 19:45:14 +0100 Subject: [PATCH 10/13] update --- camel/models/moonshot_model.py | 19 +- examples/workforce/eigent.py | 5 +- examples/workforce/run_eigent_webvoyage.py | 212 +++++++++++++-------- 3 files changed, 150 insertions(+), 86 deletions(-) diff --git a/camel/models/moonshot_model.py b/camel/models/moonshot_model.py index f001c51bff..3c89786c6e 100644 --- a/camel/models/moonshot_model.py +++ b/camel/models/moonshot_model.py @@ -164,15 +164,21 @@ def remove_null_from_schema(schema: Any) -> Any: any_of = schema['anyOf'] # Filter out null types filtered = [ - item for item in any_of - if not (isinstance(item, dict) and item.get('type') == 'null') + item + for item in any_of + if not ( + isinstance(item, dict) + and item.get('type') == 'null' + ) ] # If only one type remains, simplify if len(filtered) == 1: return remove_null_from_schema(filtered[0]) elif len(filtered) > 1: schema = schema.copy() - schema['anyOf'] = [remove_null_from_schema(item) for item in filtered] + schema['anyOf'] = [ + remove_null_from_schema(item) for item in filtered + ] return schema else: # All were null, return string type as fallback @@ -180,8 +186,7 @@ def remove_null_from_schema(schema: Any) -> Any: # Recursively process nested dicts return { - k: remove_null_from_schema(v) - for k, v in schema.items() + k: remove_null_from_schema(v) for k, v in schema.items() } elif isinstance(schema, list): return [remove_null_from_schema(item) for item in schema] @@ -193,7 +198,9 @@ def remove_null_from_schema(schema: Any) -> Any: 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']) + params['properties'] = remove_null_from_schema( + params['properties'] + ) return cleaned_tools diff --git a/examples/workforce/eigent.py b/examples/workforce/eigent.py index 942c54f884..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__) @@ -322,7 +321,7 @@ def search_agent_factory( "browser_enter", "browser_switch_tab", "browser_visit_page", - "browser_get_page_snapshot" + "browser_get_page_snapshot", ] USER_DATA_DIR = "/Users/puzhen/Desktop/pre/camel_project/camel/UserData" @@ -365,7 +364,7 @@ def search_agent_factory( *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, diff --git a/examples/workforce/run_eigent_webvoyage.py b/examples/workforce/run_eigent_webvoyage.py index a59c67cc6e..0c94596feb 100644 --- a/examples/workforce/run_eigent_webvoyage.py +++ b/examples/workforce/run_eigent_webvoyage.py @@ -17,7 +17,15 @@ import json import os import platform -import uuid + +# 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 @@ -33,15 +41,6 @@ ) from camel.types import ModelPlatformType, ModelType -# 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, -) - logger = get_logger(__name__) WORKING_DIRECTORY = os.environ.get("CAMEL_WORKDIR") or os.path.abspath( @@ -49,7 +48,9 @@ ) -def load_tasks_from_jsonl(jsonl_path: str, start_index: int = 0, end_index: int = None): +def load_tasks_from_jsonl( + jsonl_path: str, start_index: int = 0, end_index: int = None +): """ Load tasks from a JSONL file. @@ -72,7 +73,9 @@ def load_tasks_from_jsonl(jsonl_path: str, start_index: int = 0, end_index: int return tasks -async def verify_result_with_agent(task_content: str, result: dict, model_backend: BaseModelBackend): +async def verify_result_with_agent( + task_content: str, result: dict, model_backend: BaseModelBackend +): """ Verify the workforce result using a chat agent. @@ -111,7 +114,10 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen 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: + if ( + isinstance(log_entry, dict) + and 'content' in log_entry + ): final_response = log_entry.get('content') break elif 'final_result' in detailed_logs: @@ -132,11 +138,13 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen # Add final response if extracted if final_response: - verification_prompt_parts.extend([ - "Final Agent Response:", - str(final_response), - "", - ]) + verification_prompt_parts.extend( + [ + "Final Agent Response:", + str(final_response), + "", + ] + ) # Add condensed log information if available if detailed_logs: @@ -146,32 +154,37 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen 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( + [ + "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?", "", - ]) - - 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"', - "}", - ]) + "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 - )) + response = verifier_agent.step( + BaseMessage.make_user_message( + role_name="Verifier", content=verification_prompt + ) + ) try: # Try to parse JSON from the response @@ -186,12 +199,12 @@ async def verify_result_with_agent(task_content: str, result: dict, model_backen # Fallback if no JSON found verification_result = { "success": False, - "reasoning": "Failed to parse verification response" + "reasoning": "Failed to parse verification response", } except Exception as e: verification_result = { "success": False, - "reasoning": f"Error parsing verification: {str(e)}" + "reasoning": f"Error parsing verification: {e!s}", } return verification_result @@ -256,7 +269,6 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): access and can resolve a wide range of issues. """ ), - enable_tool_output_cache=True, model=model_backend_reason, tools=[ @@ -316,7 +328,9 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): # Create agents using factory functions # search_agent_factory now returns (agent, browser_toolkit) - search_agent, browser_toolkit = search_agent_factory(model_backend, task_id) + search_agent, browser_toolkit = search_agent_factory( + model_backend, task_id + ) # document_agent = document_agent_factory( # model_backend_reason, @@ -347,19 +361,19 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): "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, + # ).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: @@ -377,7 +391,9 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): # 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" + log_file_path = ( + f"eigent_logs_task_{task_index}_{human_task.id}.json" + ) else: log_file_path = f"eigent_logs_{human_task.id}.json" @@ -388,7 +404,7 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): return { "kpis": kpis, "log_tree": workforce.get_workforce_log_tree(), - "log_file": log_file_path + "log_file": log_file_path, } finally: # IMPORTANT: Close browser after each task to prevent resource leaks @@ -405,16 +421,28 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): 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') + 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'})") + 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( @@ -432,7 +460,9 @@ 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"Processing Task {actual_idx}: {task_data.get('id', 'unknown')}" + ) print(f"{'='*80}") # Combine ques and web fields @@ -446,13 +476,17 @@ async def process_all_tasks(): try: # Run the workforce with task index for unique log files - result = await run_eigent_workforce(human_task, task_index=actual_idx) + 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) + verification = await verify_result_with_agent( + task_content, result, verifier_model + ) - print(f"\nVerification Result:") + print("\nVerification Result:") print(f" Success: {verification['success']}") print(f" Reasoning: {verification['reasoning']}") @@ -462,21 +496,29 @@ async def process_all_tasks(): "task_id": task_data.get('id', str(actual_idx)), "task_content": task_content, "workforce_result": result, - "verification": verification + "verification": verification, } all_results.append(task_result) - # Save intermediate results + # 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"Log file saved to: {result['log_file']}") - print(f"Results saved to: {results_file}") + 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}: {str(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)), @@ -484,11 +526,25 @@ async def process_all_tasks(): "error": str(e), "verification": { "success": False, - "reasoning": f"Exception occurred: {str(e)}" - } + "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()) @@ -496,4 +552,6 @@ async def process_all_tasks(): 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") + print( + f"Results saved to: all_results_{args.start}_{args.end if args.end else 'end'}.json" + ) From ff1afd15898f560c87cf38cacb0e49f83539e00f Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 16 Oct 2025 11:40:23 +0100 Subject: [PATCH 11/13] update --- camel/models/base_model.py | 3 +- camel/models/moonshot_model.py | 65 +++--- .../hybrid_browser_toolkit_example.py | 34 +-- examples/workforce/run_eigent.py | 203 +++++++++++------- examples/workforce/run_eigent_webvoyage.py | 9 +- 5 files changed, 185 insertions(+), 129 deletions(-) diff --git a/camel/models/base_model.py b/camel/models/base_model.py index 297e62c5d8..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", "True").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 3c89786c6e..27e5d9ce27 100644 --- a/camel/models/moonshot_model.py +++ b/camel/models/moonshot_model.py @@ -159,35 +159,48 @@ def _clean_tool_schemas( def remove_null_from_schema(schema: Any) -> Any: """Recursively remove null types from schema.""" if isinstance(schema, dict): - # Handle anyOf with null types - if 'anyOf' in schema: - any_of = schema['anyOf'] - # Filter out null types - filtered = [ - item - for item in any_of - if not ( - isinstance(item, dict) - and item.get('type') == 'null' - ) - ] - # If only one type remains, simplify - if len(filtered) == 1: - return remove_null_from_schema(filtered[0]) - elif len(filtered) > 1: - schema = schema.copy() - schema['anyOf'] = [ - remove_null_from_schema(item) for item in filtered + # 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' + ) ] - return schema + 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: - # All were null, return string type as fallback - return {"type": "string"} + # Recursively process other values + result[key] = remove_null_from_schema(value) - # Recursively process nested dicts - return { - k: remove_null_from_schema(v) for k, v in schema.items() - } + return result elif isinstance(schema, list): return [remove_null_from_schema(item) for item in schema] else: diff --git a/examples/toolkits/hybrid_browser_toolkit_example.py b/examples/toolkits/hybrid_browser_toolkit_example.py index b356880603..3f891064ce 100644 --- a/examples/toolkits/hybrid_browser_toolkit_example.py +++ b/examples/toolkits/hybrid_browser_toolkit_example.py @@ -36,8 +36,9 @@ 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,20 +106,19 @@ async def main() -> None: - await web_toolkit_custom.browser_open() - # await web_toolkit_custom.browser_close() - # try: - # response = await agent.astep(TASK_PROMPT) - # print("Task:", TASK_PROMPT) - # print(f"Using user data directory: {USER_DATA_DIR}") - # print(f"Enabled tools: {web_toolkit_custom.enabled_tools}") - # print("\nResponse from agent:") - # print(response.msgs[0].content if response.msgs else "") - # finally: - # # Ensure browser is closed properly - # print("\nClosing browser...") - # await web_toolkit_custom.browser_close() - # print("Browser closed successfully.") + # await web_toolkit_custom.browser_open() + try: + response = await agent.astep(TASK_PROMPT) + print("Task:", TASK_PROMPT) + print(f"Using user data directory: {USER_DATA_DIR}") + print(f"Enabled tools: {web_toolkit_custom.enabled_tools}") + print("\nResponse from agent:") + print(response.msgs[0].content if response.msgs else "") + finally: + # Ensure browser is closed properly + print("\nClosing browser...") + await web_toolkit_custom.browser_close() + print("Browser closed successfully.") if __name__ == "__main__": diff --git a/examples/workforce/run_eigent.py b/examples/workforce/run_eigent.py index 77ed146418..0ee7fd32d8 100644 --- a/examples/workforce/run_eigent.py +++ b/examples/workforce/run_eigent.py @@ -16,31 +16,26 @@ import datetime import os import platform -import uuid + +# 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.messages.base import BaseMessage -from camel.models import BaseModelBackend, ModelFactory +from camel.models import 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 -# 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, -) - logger = get_logger(__name__) WORKING_DIRECTORY = os.environ.get("CAMEL_WORKDIR") or os.path.abspath( @@ -71,9 +66,9 @@ async def run_eigent_workforce(human_task: Task): # Create a single model backend for all agents model_backend = ModelFactory.create( - model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_4_1, - # url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + 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, }, @@ -87,9 +82,9 @@ async def run_eigent_workforce(human_task: Task): # }, # ) model_backend_reason = ModelFactory.create( - model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_4_1, - # url="https://api.moonshot.ai/v1", # Explicitly specify Moonshot API URL + 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, }, @@ -181,7 +176,10 @@ async def run_eigent_workforce(human_task: Task): ) # Create agents using factory functions - search_agent = search_agent_factory(model_backend, task_id) + # 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, @@ -212,69 +210,118 @@ async def run_eigent_workforce(human_task: Task): "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, + # ).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, ) - # 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}") + 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. - return { - "kpis": kpis, - "log_tree": workforce.get_workforce_log_tree(), - "log_file": log_file_path - } + 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 - 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 home page edit this Next Step as “book a meeting with + the contact name and phone number.” - - -如果type失败可能是因为ref发生了变化,你需要查看browser_get_page_snapshot - """ - ), - id='0', - ) - - # Run the workforce - result = asyncio.run(run_eigent_workforce(human_task)) - print("\n=== Execution Complete ===") - print(f"Log file saved to: {result['log_file']}") + # 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 index 0c94596feb..68326f4416 100644 --- a/examples/workforce/run_eigent_webvoyage.py +++ b/examples/workforce/run_eigent_webvoyage.py @@ -289,6 +289,7 @@ async def run_eigent_workforce(human_task: Task, task_index: int = None): 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( @@ -501,9 +502,7 @@ async def process_all_tasks(): 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" - ) + 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) @@ -532,9 +531,7 @@ async def process_all_tasks(): 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" - ) + 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) From c4c5399d58bc0821be2b30fc4f804b9efe0f4417 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 16 Oct 2025 12:19:13 +0100 Subject: [PATCH 12/13] update --- camel/models/moonshot_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/camel/models/moonshot_model.py b/camel/models/moonshot_model.py index 27e5d9ce27..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, From 6924081607e7c45c0eb8d6f0fda873ae18108d53 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 6 Nov 2025 22:05:39 +0100 Subject: [PATCH 13/13] update --- camel/agents/chat_agent.py | 211 ++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) 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, )