deepset-ai/haystack · error · ValueError
Assistant message must contain only text, tool call or…
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.
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.
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
- 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.
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
- Assistant message must contain at least one text or tool…
- Assistant message must contain one text part at most.
- System message must contain a text part.
- System message must contain only one text part.
- Tool message must contain only one tool call result.
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/b5baa3efc3b39040.
Report an issue: GitHub.
Appendix: 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)