Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,12 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
llm_response.tools_call_args,
llm_response.tools_call_ids,
):
if not func_tool_name or not func_tool_id:
logger.warning(
f"Skipping tool call with missing name or id: "
f"name={func_tool_name!r}, id={func_tool_id!r}, args={func_tool_args!r}"
)
continue
tool_result_blocks_start = len(tool_call_result_blocks)
tool_call_streak = self._track_tool_call_streak(func_tool_name)
yield _HandleFunctionToolsResult.from_message_chain(
Expand Down
34 changes: 23 additions & 11 deletions astrbot/core/provider/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,36 +452,48 @@ def to_openai_tool_calls(self) -> list[dict]:
"""Convert to OpenAI tool calls format. Deprecated, use to_openai_to_calls_model instead."""
ret = []
for idx, tool_call_arg in enumerate(self.tools_call_args):
tool_name = self.tools_call_name[idx]
tool_id = self.tools_call_ids[idx]
if not tool_name or not tool_id:
Comment on lines 454 to +457
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.

medium

Accessing self.tools_call_name and self.tools_call_ids by index idx can raise an IndexError if the parallel lists are mismatched in length. Since this similar logic is implemented in multiple places, refactor it into a shared helper function to avoid code duplication.

Suggested change
for idx, tool_call_arg in enumerate(self.tools_call_args):
tool_name = self.tools_call_name[idx]
tool_id = self.tools_call_ids[idx]
if not tool_name or not tool_id:
for tool_id, tool_name, tool_call_arg in self._get_safe_tool_calls():
References
  1. When implementing similar functionality for different cases, refactor the logic into a shared helper function to avoid code duplication.

logger.warning(
f"Skipping tool call at index {idx} because function.name or id is empty/None. "
f"tool_call_id={tool_id!r}, tool_name={tool_name!r}, arguments={tool_call_arg!r}"
)
continue
payload = {
"id": self.tools_call_ids[idx],
"id": tool_id,
"function": {
"name": self.tools_call_name[idx],
"name": tool_name,
"arguments": json.dumps(tool_call_arg),
},
"type": "function",
}
if self.tools_call_extra_content.get(self.tools_call_ids[idx]):
payload["extra_content"] = self.tools_call_extra_content[
self.tools_call_ids[idx]
]
if self.tools_call_extra_content.get(tool_id):
payload["extra_content"] = self.tools_call_extra_content[tool_id]
ret.append(payload)
return ret

def to_openai_to_calls_model(self) -> list[ToolCall]:
"""The same as to_openai_tool_calls but return pydantic model."""
ret = []
for idx, tool_call_arg in enumerate(self.tools_call_args):
tool_name = self.tools_call_name[idx]
tool_id = self.tools_call_ids[idx]
if not tool_name or not tool_id:
Comment on lines 479 to +482
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.

medium

Accessing self.tools_call_name and self.tools_call_ids by index idx can raise an IndexError if the parallel lists are mismatched in length. Since this similar logic is implemented in multiple places, refactor it into a shared helper function to avoid code duplication.

Suggested change
for idx, tool_call_arg in enumerate(self.tools_call_args):
tool_name = self.tools_call_name[idx]
tool_id = self.tools_call_ids[idx]
if not tool_name or not tool_id:
for tool_id, tool_name, tool_call_arg in self._get_safe_tool_calls():
References
  1. When implementing similar functionality for different cases, refactor the logic into a shared helper function to avoid code duplication.

logger.warning(
f"Skipping tool call at index {idx} because function.name or id is empty/None. "
f"tool_call_id={tool_id!r}, tool_name={tool_name!r}, arguments={tool_call_arg!r}"
)
continue
ret.append(
ToolCall(
id=self.tools_call_ids[idx],
id=tool_id,
function=ToolCall.FunctionBody(
name=self.tools_call_name[idx],
name=tool_name,
arguments=json.dumps(tool_call_arg),
),
# the extra_content will not serialize if it's None when calling ToolCall.model_dump()
extra_content=self.tools_call_extra_content.get(
self.tools_call_ids[idx]
),
extra_content=self.tools_call_extra_content.get(tool_id),
),
)
return ret
Expand Down
30 changes: 25 additions & 5 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,7 @@ async def _parse_openai_completion(
func_name_ls = []
tool_call_ids = []
tool_call_extra_content_dict = {}
skipped_count = 0
for tool_call in choice.message.tool_calls:
if isinstance(tool_call, str):
# workaround for #1359
Expand All @@ -945,6 +946,19 @@ async def _parse_openai_completion(
raise Exception("工具集未提供")

if tool_call.type == "function":
if (
not tool_call.function
or not tool_call.function.name
or not tool_call.id
):
logger.warning(
f"Skipping tool call with missing function, name or id: "
f"function={tool_call.function!r}, "
f"name={getattr(tool_call.function, 'name', None)!r}, "
f"id={tool_call.id!r}"
)
skipped_count += 1
continue
# workaround for #1454
if isinstance(tool_call.function.arguments, str):
try:
Expand All @@ -966,11 +980,17 @@ async def _parse_openai_completion(
if extra_content is not None:
tool_call_extra_content_dict[tool_call.id] = extra_content

llm_response.role = "tool"
llm_response.tools_call_args = args_ls
llm_response.tools_call_name = func_name_ls
llm_response.tools_call_ids = tool_call_ids
llm_response.tools_call_extra_content = tool_call_extra_content_dict
if args_ls:
llm_response.role = "tool"
llm_response.tools_call_args = args_ls
llm_response.tools_call_name = func_name_ls
llm_response.tools_call_ids = tool_call_ids
llm_response.tools_call_extra_content = tool_call_extra_content_dict
elif skipped_count > 0:
logger.warning(
f"All {skipped_count} function tool call(s) were skipped due to "
"missing name, id, or function field. Treating as non-tool-call response."
)
# specially handle finish reason
if choice.finish_reason == "content_filter":
raise Exception(
Expand Down
Loading