{"record":{"id":"b5baa3efc3b39040","repo":"deepset-ai/haystack","slug":"assistant-message-must-contain-only-text-tool-cal","errorCode":null,"errorMessage":"Assistant message must contain only text, tool call or reasoning parts.","messagePattern":"Assistant message must contain only text, tool call or reasoning parts\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/utils/jinja2_chat_extension.py","lineNumber":385,"sourceCode":"\n        if role == \"system\":\n            if not isinstance(parts[0], TextContent):\n                raise ValueError(\"System message must contain a text part.\")\n            text = parts[0].text\n            if len(parts) > 1:\n                raise ValueError(\"System message must contain only one text part.\")\n            return ChatMessage.from_system(meta=meta, name=name, text=text)\n\n        if role == \"assistant\":\n            texts = [part.text for part in parts if isinstance(part, TextContent)]\n            tool_calls = [part for part in parts if isinstance(part, ToolCall)]\n            reasoning = [part for part in parts if isinstance(part, ReasoningContent)]\n            if len(texts) > 1:\n                raise ValueError(\"Assistant message must contain one text part at most.\")\n            if len(texts) == 0 and len(tool_calls) == 0:\n                raise ValueError(\"Assistant message must contain at least one text or tool call part.\")\n            if len(parts) > len(texts) + len(tool_calls) + len(reasoning):\n                raise ValueError(\"Assistant message must contain only text, tool call or reasoning parts.\")\n            return ChatMessage.from_assistant(\n                meta=meta,\n                name=name,\n                text=texts[0] if texts else None,\n                tool_calls=tool_calls or None,\n                reasoning=reasoning[0] if reasoning else None,\n            )\n\n        if role == \"tool\":\n            tool_call_results = [part for part in parts if isinstance(part, ToolCallResult)]\n            if len(tool_call_results) == 0 or len(tool_call_results) > 1 or len(parts) > len(tool_call_results):\n                raise ValueError(\"Tool message must contain only one tool call result.\")\n\n            tool_result = tool_call_results[0].result\n            origin = tool_call_results[0].origin\n            error = tool_call_results[0].error\n\n            return ChatMessage.from_tool(meta=meta, tool_result=tool_result, origin=origin, error=error)","sourceCodeStart":367,"sourceCodeEnd":403,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/utils/jinja2_chat_extension.py#L367-L403","documentation":"Assistant messages built from a template may only contain text, tool call, and reasoning parts. This error means some other content type (e.g. ImageContent, FileContent, ToolCallResult) appeared in an assistant message block.","triggerScenarios":"Inserting a user/tool message (carrying image or tool-result parts) into an assistant message block; mixing content part types across roles via {% insert %}; custom/unknown content part objects passed through the template.","commonSituations":"Replaying multimodal history where image parts got attributed to assistant turns; inserting tool results into assistant messages instead of tool messages; framework conversions that don't remap part types per role.","solutions":["Remove image/file/tool-result parts from assistant messages in the template.","Place tool results in 'tool' role messages and images in 'user' role messages.","If inserting history with {% insert %}, verify each message's role matches its parts.","Convert unsupported parts to text before inserting into an assistant message."],"exampleFix":"// before\nassistant parts: [TextContent(\"ok\"), ToolCallResult(result=\"42\")]\n// after\nassistant parts: [TextContent(\"ok\")]  // ToolCallResult goes in a tool message","handlingStrategy":"type-guard","validationCode":"from haystack.dataclasses import ChatMessage, TextContent, ToolCall, ReasoningContent\n\ndef validate_assistant_parts(msg: ChatMessage):\n    bad = [p for p in msg.content_parts if not isinstance(p, (TextContent, ToolCall, ReasoningContent))]\n    if bad:\n        raise TypeError(f\"Assistant message has invalid parts: {[type(p).__name__ for p in bad]}\")","typeGuard":"def is_valid_assistant_part(p) -> bool:\n    from haystack.dataclasses import TextContent, ToolCall, ReasoningContent\n    return isinstance(p, (TextContent, ToolCall, ReasoningContent))","tryCatchPattern":"try:\n    messages = renderer.run(template=tpl, variables=vars)[\"messages\"]\nexcept ValueError as e:\n    if \"only text, tool call or reasoning\" in str(e):\n        log.error(\"Foreign content part in assistant message; check inserted messages' roles\")\n    raise","preventionTips":["Only insert assistant-role messages into assistant blocks.","Route images to user messages and tool results to tool messages.","Audit history-replay templates for role/part mismatches.","Add tests covering multimodal and tool-heavy templates."],"tags":["chat-template","message-validation","assistant-message"],"backgroundTag":"invalid-message-content-part","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}