Skip to content

fix: handle tool calls gracefully when tools are unavailable#7598

Open
LovieCode wants to merge 1 commit intoAstrBotDevs:masterfrom
LovieCode:fix/tool-call-handling
Open

fix: handle tool calls gracefully when tools are unavailable#7598
LovieCode wants to merge 1 commit intoAstrBotDevs:masterfrom
LovieCode:fix/tool-call-handling

Conversation

@LovieCode
Copy link
Copy Markdown
Contributor

@LovieCode LovieCode commented Apr 16, 2026

问题描述:
当 LLM 返回 tool_calls 但工具列表为空(tools is Nonetools.func_list 为空)时,原代码会将 Agent 状态异常转换为 DONE,直接结束对话,不输出任何返回。

修复方案:
修改 openai_source.py 中的工具调用解析逻辑,当工具列表为空或工具不存在时:

  1. 记录警告日志
  2. 将请求的工具名称、ID 和空参数添加到响应中作为占位符
  3. 让 Agent 继续执行流程,而不是直接转为 DONE 状态

Modifications / 改动点

  • 修改了 astrbot/core/provider/sources/openai_source.py

  • _parse_openai_completion 方法中增加了对工具列表为空和工具不存在情况的处理

  • 使用 getattr 安全获取工具名称,兼容不同格式的 tool_call

  • This is NOT a breaking change. / 这不是一个破坏性变更。


Screenshots or Test Results / 运行截图或测试结果

修改前:
image

修改后的日志输出:
image

Agent 状态不再异常转为 DONE,而是继续处理并返回工具不存在的提示。


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了"验证步骤"和"运行截图"

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Handle LLM tool calls gracefully when requested tools are missing or unavailable instead of terminating the agent flow.

Bug Fixes:

  • Prevent conversations from incorrectly ending when the LLM returns tool calls but no tools are configured or available.
  • Fallback to placeholder tool call entries and logging when a requested tool cannot be found in the current toolset.

@dosubot dosubot bot added size:S This PR changes 10-29 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Apr 16, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • When tools are unavailable or not found, you currently append an empty {} for args_ls; consider preserving the original tool_call arguments (or a minimal subset) so downstream handlers can generate more informative error messages instead of losing context.
  • The new branch that handles tools is None or not tools.func_list still sets llm_response.role = "tool"; double-check whether downstream consumers expect actual executable tools in this role and whether a distinct role or flag would make this state easier to differentiate from a normal tool-call response.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When tools are unavailable or not found, you currently append an empty `{}` for `args_ls`; consider preserving the original `tool_call` arguments (or a minimal subset) so downstream handlers can generate more informative error messages instead of losing context.
- The new branch that handles `tools is None or not tools.func_list` still sets `llm_response.role = "tool"`; double-check whether downstream consumers expect actual executable tools in this role and whether a distinct role or flag would make this state easier to differentiate from a normal tool-call response.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="855-857" />
<code_context>
-                    raise Exception("工具集未提供")
+
+                # 安全获取工具名称
+                tool_name = getattr(
+                    getattr(tool_call, 'function', None), 'name', 
+                    getattr(tool_call, 'name', None)
+                )
+                if not tool_name:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Broaden tool_name extraction to handle dict-like tool_call objects.

Because of the earlier `json.loads(tool_call)` workaround, `tool_call` may be a dict-like object. In that case, `getattr` won’t see values like `tool_call["function"]["name"]` or `tool_call["name"]`, so `tool_name` stays empty and a valid call is skipped. Please extend this logic to also handle mapping types (e.g., `isinstance(tool_call, Mapping)` and read from keys) before falling back to `getattr`.

Suggested implementation:

```python
import json
from collections.abc import Mapping

```

```python
                # 安全获取工具名称,兼容对象属性和 Mapping 类型
                tool_name = None
                if isinstance(tool_call, Mapping):
                    function_part = tool_call.get("function")
                    if isinstance(function_part, Mapping):
                        tool_name = function_part.get("name")
                    if not tool_name:
                        tool_name = tool_call.get("name")
                else:
                    tool_name = getattr(
                        getattr(tool_call, "function", None),
                        "name",
                        getattr(tool_call, "name", None),
                    )

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/sources/openai_source.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the OpenAI completion parsing logic to handle tool calls more gracefully when the toolset is missing or a specific tool is not found. While these changes improve error handling and logging, the implementation incorrectly uses getattr to extract information from tool_call objects. Since an existing workaround can transform these objects into dictionaries, the use of getattr will fail to retrieve the tool name and ID. Feedback has been provided to ensure compatibility with both dictionary and object types.

Comment on lines +855 to +868
tool_name = getattr(
getattr(tool_call, 'function', None), 'name',
getattr(tool_call, 'name', None)
)
if not tool_name:
logger.warning(f"Could not extract tool name from tool_call: {tool_call}")
continue

if tools is None or not tools.func_list:
logger.warning(f"LLM requested tool '{tool_name}' but no tools are available")
func_name_ls.append(tool_name)
tool_call_ids.append(getattr(tool_call, 'id', ''))
args_ls.append({})
continue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

getattr 函数无法在字典(dict)上工作。由于第 852 行的 workaround 会将字符串形式的 tool_call 解析为字典,这里的 getattr(tool_call, ...) 将无法获取到任何内容,导致 tool_name 为 None 并在第 861 行跳过该工具调用。此外,第 866 行的 getattr(tool_call, 'id', '') 在处理字典时也会返回空字符串。建议根据 tool_call 的类型(字典或对象)分别处理,以确保兼容性。

                # 安全获取工具名称和 ID
                if isinstance(tool_call, dict):
                    tool_name = tool_call.get('function', {}).get('name') or tool_call.get('name')
                    tool_id = tool_call.get('id', '')
                else:
                    tool_name = getattr(getattr(tool_call, 'function', None), 'name', getattr(tool_call, 'name', None))
                    tool_id = getattr(tool_call, 'id', '')

                if not tool_name:
                    logger.warning(f"Could not extract tool name from tool_call: {tool_call}")
                    continue

                if tools is None or not tools.func_list:
                    logger.warning(f"LLM requested tool '{tool_name}' but no tools are available")
                    func_name_ls.append(tool_name)
                    tool_call_ids.append(tool_id)
                    args_ls.append({})
                    continue

else:
logger.warning(f"Tool '{tool_name}' not found in toolset")
func_name_ls.append(tool_name)
tool_call_ids.append(getattr(tool_call, 'id', ''))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

此处同样存在 getattr 无法处理字典的问题。如果采用了上方建议的归一化处理,这里应直接使用 tool_id 变量。

Suggested change
tool_call_ids.append(getattr(tool_call, 'id', ''))
tool_call_ids.append(tool_id)

@LovieCode LovieCode force-pushed the fix/tool-call-handling branch from 4da358f to 3c9d4c2 Compare April 16, 2026 13:19
@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Apr 16, 2026
@LovieCode LovieCode force-pushed the fix/tool-call-handling branch from 3c9d4c2 to 1c5135d Compare April 16, 2026 13:22
Copy link
Copy Markdown
Member

@Soulter Soulter Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我觉得更好的做法是在 tool_loop_agent_runner 去做检测

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants