Address issue with None content in tools, Closes #159

Signed-off-by: Víctor Mayoral Vilches <v.mayoralv@gmail.com>
This commit is contained in:
Víctor Mayoral Vilches 2025-05-26 14:49:10 +02:00
parent b998242f3a
commit aefb7b4d7b
3 changed files with 54 additions and 37 deletions

View File

@ -2335,6 +2335,7 @@ class OpenAIChatCompletionsModel(Model):
elif ("An assistant message with 'tool_calls'" in str(e) or
"`tool_use` blocks must be followed by a user message with `tool_result`" in str(e) or # noqa: E501 # pylint: disable=C0301
"`tool_use` ids were found without `tool_result` blocks immediately after" in str(e) or # noqa: E501 # pylint: disable=C0301
"An assistant message with 'tool_calls' must be followed by tool messages" in str(e) or
"messages with role 'tool' must be a response to a preceeding message with 'tool_calls'" in str(e)):
print(f"Error: {str(e)}")

View File

@ -358,6 +358,7 @@ def load_history_from_jsonl(file_path):
"""
messages = []
last_assistant_message = None
tool_outputs = {} # Map tool_call_id to output content
try:
with open(file_path, encoding='utf-8') as f:
@ -371,17 +372,15 @@ def load_history_from_jsonl(file_path):
print(f"Error loading line: {line}")
continue
# Collect tool outputs from tool_message events
if record.get("event") == "tool_message":
tool_call_id = record.get("tool_call_id", "")
content = record.get("content", "")
if tool_call_id and content:
tool_outputs[tool_call_id] = content
# process assistant messages and keep the last one
# for additing it manually at the end
#
# NOTE: it might be the case that if the last message is of type "tool_message"
# we might be missing it. For that purpose, leaving here the corresponding code
# in case of need:
# if entry.get("event") == "tool_message":
# tool_call_id = entry.get("tool_call_id", "")
# content = entry.get("content", "")
# if tool_call_id and content:
# tool_outputs[tool_call_id] = content
if record.get("event") == "assistant_message":
last_assistant_message = record.get("content")
@ -396,7 +395,8 @@ def load_history_from_jsonl(file_path):
# Add this message if we haven't seen it already
if not any(m.get("role") == msg.get("role") and
m.get("content") == msg.get("content") for m in messages):
m.get("content") == msg.get("content") and
m.get("tool_call_id") == msg.get("tool_call_id") for m in messages):
messages.append(msg)
# Extract assistant messages and tool responses from model record choices
@ -405,23 +405,9 @@ def load_history_from_jsonl(file_path):
if "message" in choice and "role" in choice["message"]:
msg = choice["message"]
if not any(m.get("role") == msg.get("role") and
m.get("content") == msg.get("content") for m in messages):
m.get("content") == msg.get("content") and
m.get("tool_call_id") == msg.get("tool_call_id") for m in messages):
messages.append(msg)
# Check for tool_calls in the message
if msg.get("tool_calls"):
for tool_call in msg.get("tool_calls", []):
if tool_call.get("id") and "function" in tool_call:
name = tool_call["function"].get("name", "")
arguments = tool_call["function"].get("arguments", "")
if name and arguments:
# Add a placeholder tool message - will be filled later
tool_message = {
"role": "tool",
"tool_call_id": tool_call.get("id"),
"content": ""
}
messages.append(tool_message)
except Exception as e: # pylint: disable=broad-except
print(f"Error loading history from {file_path}: {e}")
@ -430,18 +416,42 @@ def load_history_from_jsonl(file_path):
for msg in messages:
if not any(m.get("role") == msg.get("role") and
m.get("content") == msg.get("content") and
m.get("tool_call_id", "") == msg.get("tool_call_id", "") for m in unique_messages):
m.get("tool_call_id", "") == msg.get("tool_call_id", "") and
m.get("tool_calls") == msg.get("tool_calls") for m in unique_messages):
unique_messages.append(msg)
# Add last message to the end of the list
# Now add tool result messages for any tool calls that have outputs
final_messages = []
for msg in unique_messages:
final_messages.append(msg)
# If this is an assistant message with tool_calls, add corresponding tool results
if (msg.get("role") == "assistant" and
msg.get("tool_calls") and
isinstance(msg.get("tool_calls"), list)):
for tool_call in msg.get("tool_calls", []):
tool_call_id = tool_call.get("id")
if tool_call_id and tool_call_id in tool_outputs:
# Add the tool result message immediately after the assistant message
tool_result_msg = {
"role": "tool",
"tool_call_id": tool_call_id,
"content": tool_outputs[tool_call_id]
}
final_messages.append(tool_result_msg)
# Add last message to the end of the list if it exists and isn't already there
if last_assistant_message:
unique_messages.append(
{
# Check if this message is already in the list
if not any(m.get("role") == "assistant" and
m.get("content") == last_assistant_message for m in final_messages):
final_messages.append({
"role": "assistant",
"content": last_assistant_message
}
)
return unique_messages
})
return final_messages
def get_token_stats(file_path):

View File

@ -854,12 +854,18 @@ def fix_message_list(messages): # pylint: disable=R0914,R0915,R0912
# Ensure messages have non-null content (required by some providers)
for msg in sanitized_messages:
if msg.get("role") != "tool" and msg.get("content") is None and not msg.get("tool_calls"):
# For assistant messages with tool_calls, content can be None
if msg.get("role") == "assistant" and msg.get("tool_calls"):
# Assistant messages with tool calls can have None content - this is valid
pass
elif msg.get("role") != "tool" and msg.get("content") is None and not msg.get("tool_calls"):
# For non-tool messages without tool_calls, ensure content is not None
msg["content"] = ""
# For tool messages, ensure content is never null
if msg.get("role") == "tool" and msg.get("content") is None:
msg["content"] = f"Tool response for {msg.get('tool_call_id', 'unknown')}"
# For tool messages, ensure content is never null or empty
if msg.get("role") == "tool":
if msg.get("content") is None or msg.get("content") == "":
msg["content"] = f"Tool response for {msg.get('tool_call_id', 'unknown')}"
# Special case for Claude: ensure strict alternating pattern between assistant tool_calls and tool results
# If multiple consecutive assistant messages with tool_calls exist, interleave them with tool responses