{"record":{"id":"a80f3c13cf6a5ca6","repo":"deepset-ai/haystack","slug":"user-message-must-contain-only-textcontent-string","errorCode":null,"errorMessage":"User message must contain only TextContent, string, ImageContent or FileContent parts.","messagePattern":"User message must contain only TextContent, string, ImageContent or FileContent parts\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/utils/jinja2_chat_extension.py","lineNumber":363,"sourceCode":"        parts: list[ChatMessageContentT], role: str, meta: dict, name: str | None = None\n    ) -> ChatMessage:\n        \"\"\"\n        Validate the parts of a chat message and build a ChatMessage object.\n\n        :param parts: Content parts of the message\n        :param role: The role of the message\n        :param meta: The metadata of the message\n        :param name: The optional name of the message\n        :return: A ChatMessage object\n\n        :raises ValueError: If content parts don't allow to build a valid ChatMessage object or the role is not\n                            supported\n        \"\"\"\n\n        if role == \"user\":\n            valid_parts = [part for part in parts if isinstance(part, (TextContent, str, ImageContent, FileContent))]\n            if len(parts) != len(valid_parts):\n                raise ValueError(\n                    \"User message must contain only TextContent, string, ImageContent or FileContent parts.\"\n                )\n            return ChatMessage.from_user(meta=meta, name=name, content_parts=valid_parts)\n\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.\")","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/utils/jinja2_chat_extension.py#L345-L381","documentation":"A user-role message built from a chat template contained a content part that is not allowed for the user role. Only TextContent, plain strings, ImageContent, and FileContent are valid user parts; anything else (e.g. ToolCall, ToolCallResult, ReasoningContent) is rejected.","triggerScenarios":"Inserting a ChatMessage with tool-call parts into a user message block via {% insert %}; serializing an assistant/tool message's parts into a user message; a custom content part type passed through the template.","commonSituations":"Trying to replay tool calls inside a user turn; converting conversation history where roles got misassigned; inserting document objects instead of their textual content into a user message.","solutions":["Remove non-text/image/file parts from the user message in the template.","Put tool call results in a 'tool' role message instead of a user message.","If inserting mixed history with {% insert %}, ensure each inserted message has the correct role and parts.","Convert unsupported parts to TextContent, e.g. render tool results as text."],"exampleFix":"// before\nuser parts = [TextContent(\"hi\"), ToolCall(id=\"1\", ...)]\n// after\nuser parts = [TextContent(\"hi\")]  // move ToolCall to assistant message","handlingStrategy":"type-guard","validationCode":"from haystack.dataclasses import ChatMessage, TextContent, ImageContent, FileContent\n\ndef validate_user_parts(msg: ChatMessage):\n    bad = [p for p in msg.content_parts if not isinstance(p, (TextContent, str, ImageContent, FileContent))]\n    if bad:\n        raise TypeError(f\"User message has invalid parts: {[type(p).__name__ for p in bad]}\")","typeGuard":"def is_valid_user_part(p) -> bool:\n    from haystack.dataclasses import TextContent, ImageContent, FileContent\n    return isinstance(p, (TextContent, str, ImageContent, FileContent))","tryCatchPattern":"try:\n    messages = renderer.run(template=tpl, variables=vars)[\"messages\"]\nexcept ValueError as e:\n    if \"User message must contain only\" in str(e):\n        log.error(\"Non-user-role part in user message; inspect inserted messages\")\n    raise","preventionTips":["Only insert user-role ChatMessages into user message blocks.","Render tool results as text before placing them in user prompts.","Check role/part compatibility when replaying conversation history.","Assert part types in tests for every template path."],"tags":["chat-template","message-validation","content-parts"],"backgroundTag":"invalid-message-content-part","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}