From aa512fac6e6f064315b95a87a94891f52839d6a8 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Tue, 18 Mar 2025 11:46:35 +0800 Subject: [PATCH 01/11] refactor(app): Complete exception logging in LLM.ask --- app/llm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/llm.py b/app/llm.py index 18a13af..f78a7f7 100644 --- a/app/llm.py +++ b/app/llm.py @@ -309,8 +309,8 @@ class LLM: elif isinstance(oe, APIError): logger.error(f"API error: {oe}") raise - except Exception as e: - logger.error(f"Unexpected error in ask: {e}") + except Exception: + logger.exception(f"Unexpected error in ask") raise @retry( From ca612699ec0d2b1942c27a0ccd2c1adae39a1a48 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Tue, 18 Mar 2025 11:53:47 +0800 Subject: [PATCH 02/11] refactor(app): explicitly specify LLM request parameters to allow typing --- app/llm.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/llm.py b/app/llm.py index f78a7f7..b527058 100644 --- a/app/llm.py +++ b/app/llm.py @@ -263,23 +263,23 @@ class LLM: if not stream: # Non-streaming request - params["stream"] = False - - response = await self.client.chat.completions.create(**params) + response = await self.client.chat.completions.create( + **params, stream=False + ) if not response.choices or not response.choices[0].message.content: raise ValueError("Empty or invalid response from LLM") # Update token counts - self.update_token_count(response.usage.prompt_tokens) + if response.usage: + self.update_token_count(response.usage.prompt_tokens) return response.choices[0].message.content # Streaming request, For streaming, update estimated token count before making the request self.update_token_count(input_tokens) - params["stream"] = True - response = await self.client.chat.completions.create(**params) + response = await self.client.chat.completions.create(**params, stream=True) collected_messages = [] async for chunk in response: @@ -292,6 +292,8 @@ class LLM: if not full_response: raise ValueError("Empty response from streaming LLM") + # TODO Update token counts + return full_response except TokenLimitExceeded: From cf7d6c120775fce34366661a611bf73352e9f8fc Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Tue, 18 Mar 2025 13:36:15 +0800 Subject: [PATCH 03/11] chore(app): Update error logging to use exception details --- app/llm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/llm.py b/app/llm.py index b527058..6a147c4 100644 --- a/app/llm.py +++ b/app/llm.py @@ -299,11 +299,11 @@ class LLM: except TokenLimitExceeded: # Re-raise token limit errors without logging raise - except ValueError as ve: - logger.error(f"Validation error: {ve}") + except ValueError: + logger.exception(f"Validation error") raise except OpenAIError as oe: - logger.error(f"OpenAI API error: {oe}") + logger.exception(f"OpenAI API error") if isinstance(oe, AuthenticationError): logger.error("Authentication failed. Check API key.") elif isinstance(oe, RateLimitError): From 0654d36e4050b09fad031c5c7c53e98faf71adf7 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Tue, 18 Mar 2025 17:23:09 +0800 Subject: [PATCH 04/11] ci: update Markdown issue templates to forms --- .../ISSUE_TEMPLATE/request_new_features.md | 14 ------ .../ISSUE_TEMPLATE/request_new_features.yaml | 21 +++++++++ .github/ISSUE_TEMPLATE/show_me_the_bug.md | 25 ----------- .github/ISSUE_TEMPLATE/show_me_the_bug.yaml | 44 +++++++++++++++++++ 4 files changed, 65 insertions(+), 39 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/request_new_features.md create mode 100644 .github/ISSUE_TEMPLATE/request_new_features.yaml delete mode 100644 .github/ISSUE_TEMPLATE/show_me_the_bug.md create mode 100644 .github/ISSUE_TEMPLATE/show_me_the_bug.yaml diff --git a/.github/ISSUE_TEMPLATE/request_new_features.md b/.github/ISSUE_TEMPLATE/request_new_features.md deleted file mode 100644 index c191adb..0000000 --- a/.github/ISSUE_TEMPLATE/request_new_features.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "🤔 Request new features" -about: Suggest ideas or features you’d like to see implemented in OpenManus. -title: '' -labels: kind/features -assignees: '' ---- - -**Feature description** - - -**Your Feature** - - diff --git a/.github/ISSUE_TEMPLATE/request_new_features.yaml b/.github/ISSUE_TEMPLATE/request_new_features.yaml new file mode 100644 index 0000000..749ab7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request_new_features.yaml @@ -0,0 +1,21 @@ +name: "🤔 Request new features" +description: Suggest ideas or features you’d like to see implemented in OpenManus. +labels: enhancement +body: + - type: textarea + id: feature-description + attributes: + label: Feature description + description: | + Provide a clear and concise description of the proposed feature + validations: + required: true + - type: textarea + id: your-feature + attributes: + label: Your Feature + description: | + Explain your idea or implementation process, if any. Optionally, include a Pull Request URL. + Ensure accompanying docs/tests/examples are provided for review. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/show_me_the_bug.md b/.github/ISSUE_TEMPLATE/show_me_the_bug.md deleted file mode 100644 index a3d8700..0000000 --- a/.github/ISSUE_TEMPLATE/show_me_the_bug.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: "🪲 Show me the Bug" -about: Report a bug encountered while using OpenManus and seek assistance. -title: '' -labels: kind/bug -assignees: '' ---- - -**Bug description** - - -**Bug solved method** - - - -**Environment information** - - -- System version: -- Python version: -- OpenManus version or branch: -- Installation method (e.g., `pip install -r requirements.txt` or `pip install -e .`): - -**Screenshots or logs** - diff --git a/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml b/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml new file mode 100644 index 0000000..de9298e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/show_me_the_bug.yaml @@ -0,0 +1,44 @@ +name: "🪲 Show me the Bug" +description: Report a bug encountered while using OpenManus and seek assistance. +labels: bug +body: + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: | + Clearly describe the bug you encountered + validations: + required: true + - type: textarea + id: solve-method + attributes: + label: Bug solved method + description: | + If resolved, explain the solution. Optionally, include a Pull Request URL. + If unresolved, provide additional details to aid investigation + validations: + required: true + - type: textarea + id: environment-information + attributes: + label: Environment information + description: | + System: e.g., Ubuntu 22.04 + Python: e.g., 3.12 + OpenManus version: e.g., 0.1.0 + value: | + - System version: + - Python version: + - OpenManus version or branch: + - Installation method (e.g., `pip install -r requirements.txt` or `pip install -e .`): + validations: + required: true + - type: textarea + id: extra-information + attributes: + label: Extra information + description: | + For example, attach screenshots or logs to help diagnose the issue + validations: + required: false From 2fad2904d7e8f922464e1df6d25f119bc7b778b1 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Tue, 18 Mar 2025 21:10:43 +0800 Subject: [PATCH 05/11] ci(pr-autodiff): add Chinese explicit shot --- .github/workflows/pr-autodiff.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/pr-autodiff.yaml b/.github/workflows/pr-autodiff.yaml index ed218dc..85c79d0 100644 --- a/.github/workflows/pr-autodiff.yaml +++ b/.github/workflows/pr-autodiff.yaml @@ -86,6 +86,17 @@ jobs: ### Spelling/Offensive Content Check - No spelling mistakes or offensive content found in the code or comments. + + ## 中文(简体) + - 新增了 `ABC` 类 + - `foo` 模块中的 `f()` 行为已修复 + + ### 评论高亮 + - `config.toml` 需要正确配置才能确保新功能正常运行。 + + ### 内容检查 + - 没有发现代码或注释中的拼写错误或不当措辞。 + 3. Highlight non-English comments 4. Check for spelling/offensive content''' From d7b3f9a5c3e87f31ff61fe8baa9939b5038c1a7b Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 13:32:31 +0800 Subject: [PATCH 06/11] fix(pr-autodiff): make sure compare does correctly --- .github/workflows/pr-autodiff.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-autodiff.yaml b/.github/workflows/pr-autodiff.yaml index 85c79d0..46c95c6 100644 --- a/.github/workflows/pr-autodiff.yaml +++ b/.github/workflows/pr-autodiff.yaml @@ -15,21 +15,20 @@ jobs: (github.event_name == 'pull_request') || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '!pr-diff') && - (github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && + (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && github.event.issue.pull_request) steps: - name: Get PR head SHA id: get-pr-sha run: | - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "pr_sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT - echo "Retrieved PR head SHA: ${{ github.event.pull_request.head.sha }}" - else - PR_URL="${{ github.event.issue.pull_request.url }}" - SHA=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL | jq -r '.head.sha') - echo "pr_sha=$SHA" >> $GITHUB_OUTPUT - echo "Retrieved PR head SHA from API: $SHA" - fi + PR_URL="${{ github.event.issue.pull_request.url || github.event.pull_request.url }}" + # https://api.github.com/repos/OpenManus/pulls/1 + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL) + SHA=$(echo $RESPONSE | jq -r '.head.sha') + TARGET_BRANCH=$(echo $RESPONSE | jq -r '.base.ref') + echo "pr_sha=$SHA" >> $GITHUB_OUTPUT + echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT + echo "Retrieved PR head SHA from API: $SHA, target branch: $TARGET_BRANCH" - name: Check out code uses: actions/checkout@v4 with: @@ -49,6 +48,7 @@ jobs: OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + TARGET_BRANCH: ${{ steps.get-pr-sha.outputs.target_branch }} run: |- cat << 'EOF' > /tmp/_workflow_core.py import os @@ -59,7 +59,7 @@ jobs: def get_diff(): result = subprocess.run( - ['git', 'diff', 'origin/main...HEAD'], + ['git', 'diff', 'origin/' + os.getenv('TARGET_BRANCH') + '...HEAD'], capture_output=True, text=True, check=True) return '\n'.join( line for line in result.stdout.split('\n') From 94e2ab7c86607505807def45ddd3c4709ac85969 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 14:09:46 +0800 Subject: [PATCH 07/11] fix(llm): accept empty choices as valid response and handle that case gracefully --- app/agent/toolcall.py | 37 +++++++++++++++++++------------------ app/llm.py | 10 +++++++--- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/agent/toolcall.py b/app/agent/toolcall.py index 131fd91..b9fab70 100644 --- a/app/agent/toolcall.py +++ b/app/agent/toolcall.py @@ -10,7 +10,6 @@ from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice from app.tool import CreateChatCompletion, Terminate, ToolCollection - TOOL_CALL_REQUIRED = "Tool calls required but none provided" @@ -71,40 +70,42 @@ class ToolCallAgent(ReActAgent): return False raise - self.tool_calls = response.tool_calls + self.tool_calls = tool_calls = ( + response.tool_calls if response and response.tool_calls else [] + ) + content = response.content if response and response.content else "" # Log response info - logger.info(f"✨ {self.name}'s thoughts: {response.content}") + logger.info(f"✨ {self.name}'s thoughts: {content}") logger.info( - f"🛠️ {self.name} selected {len(response.tool_calls) if response.tool_calls else 0} tools to use" + f"🛠️ {self.name} selected {len(tool_calls) if tool_calls else 0} tools to use" ) - if response.tool_calls: + if tool_calls: logger.info( - f"🧰 Tools being prepared: {[call.function.name for call in response.tool_calls]}" - ) - logger.info( - f"🔧 Tool arguments: {response.tool_calls[0].function.arguments}" + f"🧰 Tools being prepared: {[call.function.name for call in tool_calls]}" ) + logger.info(f"🔧 Tool arguments: {tool_calls[0].function.arguments}") try: + if response is None: + raise RuntimeError("No response received from the LLM") + # Handle different tool_choices modes if self.tool_choices == ToolChoice.NONE: - if response.tool_calls: + if tool_calls: logger.warning( f"🤔 Hmm, {self.name} tried to use tools when they weren't available!" ) - if response.content: - self.memory.add_message(Message.assistant_message(response.content)) + if content: + self.memory.add_message(Message.assistant_message(content)) return True return False # Create and add assistant message assistant_msg = ( - Message.from_tool_calls( - content=response.content, tool_calls=self.tool_calls - ) + Message.from_tool_calls(content=content, tool_calls=self.tool_calls) if self.tool_calls - else Message.assistant_message(response.content) + else Message.assistant_message(content) ) self.memory.add_message(assistant_msg) @@ -113,7 +114,7 @@ class ToolCallAgent(ReActAgent): # For 'auto' mode, continue with content if no commands but content exists if self.tool_choices == ToolChoice.AUTO and not self.tool_calls: - return bool(response.content) + return bool(content) return bool(self.tool_calls) except Exception as e: @@ -209,7 +210,7 @@ class ToolCallAgent(ReActAgent): return f"Error: {error_msg}" except Exception as e: error_msg = f"⚠️ Tool '{name}' encountered a problem: {str(e)}" - logger.error(error_msg) + logger.exception(error_msg) return f"Error: {error_msg}" async def _handle_special_tool(self, name: str, result: Any, **kwargs): diff --git a/app/llm.py b/app/llm.py index f99b6b1..1a4e05b 100644 --- a/app/llm.py +++ b/app/llm.py @@ -10,6 +10,7 @@ from openai import ( OpenAIError, RateLimitError, ) +from openai.types.chat.chat_completion_message import ChatCompletionMessage from tenacity import ( retry, retry_if_exception_type, @@ -653,7 +654,7 @@ class LLM: tool_choice: TOOL_CHOICE_TYPE = ToolChoice.AUTO, # type: ignore temperature: Optional[float] = None, **kwargs, - ): + ) -> ChatCompletionMessage | None: """ Ask LLM using functions/tools and return the response. @@ -731,12 +732,15 @@ class LLM: temperature if temperature is not None else self.temperature ) - response = await self.client.chat.completions.create(**params) + response: ChatCompletion = await self.client.chat.completions.create( + **params, stream=False + ) # Check if response is valid if not response.choices or not response.choices[0].message: print(response) - raise ValueError("Invalid or empty response from LLM") + # raise ValueError("Invalid or empty response from LLM") + return None # Update token counts self.update_token_count( From 7e3609f19fb29412d57e2556af77ebc2f968e02d Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 14:10:07 +0800 Subject: [PATCH 08/11] chore(file_operators): use utf-8 as default encoding --- app/tool/file_operators.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/tool/file_operators.py b/app/tool/file_operators.py index 61f8b16..5720172 100644 --- a/app/tool/file_operators.py +++ b/app/tool/file_operators.py @@ -42,17 +42,20 @@ class FileOperator(Protocol): class LocalFileOperator(FileOperator): """File operations implementation for local filesystem.""" + def __init__(self, encoding: str = "utf-8"): + self.encoding = encoding + async def read_file(self, path: PathLike) -> str: """Read content from a local file.""" try: - return Path(path).read_text() + return Path(path).read_text(encoding=self.encoding) except Exception as e: raise ToolError(f"Failed to read {path}: {str(e)}") from None async def write_file(self, path: PathLike, content: str) -> None: """Write content to a local file.""" try: - Path(path).write_text(content) + Path(path).write_text(content, encoding=self.encoding) except Exception as e: raise ToolError(f"Failed to write to {path}: {str(e)}") from None From d644d976b0bf0d791b96f4fe883b9544457a0836 Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 14:12:46 +0800 Subject: [PATCH 09/11] fix: pre-commit --- app/agent/toolcall.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/agent/toolcall.py b/app/agent/toolcall.py index b9fab70..76f6f01 100644 --- a/app/agent/toolcall.py +++ b/app/agent/toolcall.py @@ -10,6 +10,7 @@ from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice from app.tool import CreateChatCompletion, Terminate, ToolCollection + TOOL_CALL_REQUIRED = "Tool calls required but none provided" From b9df45bc68e8cb995b47ba9d40d75e3d47db2f8c Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 15:44:06 +0800 Subject: [PATCH 10/11] chore(app/tool): remove constructor in LocalFileOperator --- app/tool/file_operators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/tool/file_operators.py b/app/tool/file_operators.py index 5720172..dd64c83 100644 --- a/app/tool/file_operators.py +++ b/app/tool/file_operators.py @@ -42,8 +42,7 @@ class FileOperator(Protocol): class LocalFileOperator(FileOperator): """File operations implementation for local filesystem.""" - def __init__(self, encoding: str = "utf-8"): - self.encoding = encoding + encoding: str = "utf-8" async def read_file(self, path: PathLike) -> str: """Read content from a local file.""" From 44243a1b979ed5ee11a4d88085aea439494b611e Mon Sep 17 00:00:00 2001 From: Sheng Fan Date: Wed, 19 Mar 2025 16:57:12 +0800 Subject: [PATCH 11/11] fix(browser_use_tool): reimplement screenshot logics to get JPEG data --- app/tool/browser_use_tool.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/tool/browser_use_tool.py b/app/tool/browser_use_tool.py index 7fe8f16..0158e07 100644 --- a/app/tool/browser_use_tool.py +++ b/app/tool/browser_use_tool.py @@ -1,4 +1,5 @@ import asyncio +import base64 import json from typing import Generic, Optional, TypeVar @@ -552,7 +553,16 @@ Page content: viewport_height = ctx.config.browser_window_size.get("height", 0) # Take a screenshot for the state - screenshot = await ctx.take_screenshot(full_page=True) + page = await ctx.get_current_page() + + await page.bring_to_front() + await page.wait_for_load_state() + + screenshot = await page.screenshot( + full_page=True, animations="disabled", type="jpeg", quality=100 + ) + + screenshot = base64.b64encode(screenshot).decode("utf-8") # Build the state info with all required fields state_info = {