diff --git a/src/cai/sdk/agents/models/openai_chatcompletions.py b/src/cai/sdk/agents/models/openai_chatcompletions.py index b5a22469..3411db13 100644 --- a/src/cai/sdk/agents/models/openai_chatcompletions.py +++ b/src/cai/sdk/agents/models/openai_chatcompletions.py @@ -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)}") diff --git a/src/cai/sdk/agents/run_to_jsonl.py b/src/cai/sdk/agents/run_to_jsonl.py index a8197090..686cb54e 100644 --- a/src/cai/sdk/agents/run_to_jsonl.py +++ b/src/cai/sdk/agents/run_to_jsonl.py @@ -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): diff --git a/src/cai/util.py b/src/cai/util.py index fabe9a37..cfb9cce7 100644 --- a/src/cai/util.py +++ b/src/cai/util.py @@ -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