deepset-ai/haystack · error · ValueError

Assistant message must contain only text, tool call or reaso

Error message

Assistant message must contain only text, tool call or reasoning parts.

What it means

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.

Source

Thrown at haystack/utils/jinja2_chat_extension.py:385

        if role == "system":
            if not isinstance(parts[0], TextContent):
                raise ValueError("System message must contain a text part.")
            text = parts[0].text
            if len(parts) > 1:
                raise ValueError("System message must contain only one text part.")
            return ChatMessage.from_system(meta=meta, name=name, text=text)

        if role == "assistant":
            texts = [part.text for part in parts if isinstance(part, TextContent)]
            tool_calls = [part for part in parts if isinstance(part, ToolCall)]
            reasoning = [part for part in parts if isinstance(part, ReasoningContent)]
            if len(texts) > 1:
                raise ValueError("Assistant message must contain one text part at most.")
            if len(texts) == 0 and len(tool_calls) == 0:
                raise ValueError("Assistant message must contain at least one text or tool call part.")
            if len(parts) > len(texts) + len(tool_calls) + len(reasoning):
                raise ValueError("Assistant message must contain only text, tool call or reasoning parts.")
            return ChatMessage.from_assistant(
                meta=meta,
                name=name,
                text=texts[0] if texts else None,
                tool_calls=tool_calls or None,
                reasoning=reasoning[0] if reasoning else None,
            )

        if role == "tool":
            tool_call_results = [part for part in parts if isinstance(part, ToolCallResult)]
            if len(tool_call_results) == 0 or len(tool_call_results) > 1 or len(parts) > len(tool_call_results):
                raise ValueError("Tool message must contain only one tool call result.")

            tool_result = tool_call_results[0].result
            origin = tool_call_results[0].origin
            error = tool_call_results[0].error

            return ChatMessage.from_tool(meta=meta, tool_result=tool_result, origin=origin, error=error)

View on GitHub (pinned to e318778c9b)

Solutions

  1. Remove image/file/tool-result parts from assistant messages in the template.
  2. Place tool results in 'tool' role messages and images in 'user' role messages.
  3. If inserting history with {% insert %}, verify each message's role matches its parts.
  4. Convert unsupported parts to text before inserting into an assistant message.

Example fix

// before
assistant parts: [TextContent("ok"), ToolCallResult(result="42")]
// after
assistant parts: [TextContent("ok")]  // ToolCallResult goes in a tool message
Defensive patterns

Strategy: type-guard

Validate before calling

from haystack.dataclasses import ChatMessage, TextContent, ToolCall, ReasoningContent

def validate_assistant_parts(msg: ChatMessage):
    bad = [p for p in msg.content_parts if not isinstance(p, (TextContent, ToolCall, ReasoningContent))]
    if bad:
        raise TypeError(f"Assistant message has invalid parts: {[type(p).__name__ for p in bad]}")

Type guard

def is_valid_assistant_part(p) -> bool:
    from haystack.dataclasses import TextContent, ToolCall, ReasoningContent
    return isinstance(p, (TextContent, ToolCall, ReasoningContent))

Try / catch

try:
    messages = renderer.run(template=tpl, variables=vars)["messages"]
except ValueError as e:
    if "only text, tool call or reasoning" in str(e):
        log.error("Foreign content part in assistant message; check inserted messages' roles")
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/b5baa3efc3b39040. Report an issue: GitHub.