deepset-ai/haystack · error

Each ChatMessage response must have the 'assistant' role, go

Error message

Each ChatMessage response must have the 'assistant' role, got '{item.role.value}'.

What it means

Canned ChatMessage responses for MockChatGenerator must have the assistant role, since the mock simulates the model's reply. Passing a ChatMessage with another role (user, system, tool) raises ValueError stating the actual role found.

Source

Thrown at haystack/components/generators/chat/mock.py:164

        items: list[str | ChatMessage]
        if isinstance(responses, (str, ChatMessage)):
            items = [responses]
        elif isinstance(responses, Sequence):
            items = list(responses)
        else:
            raise TypeError(f"'responses' must be a string, ChatMessage, or a sequence of them, got {type(responses)}.")

        if len(items) == 0:
            raise ValueError("'responses' must not be an empty list.")

        normalized: list[ChatMessage] = []
        for item in items:
            if isinstance(item, str):
                normalized.append(ChatMessage.from_assistant(item))
            elif isinstance(item, ChatMessage):
                if item.role != ChatRole.ASSISTANT:
                    raise ValueError(
                        f"Each ChatMessage response must have the 'assistant' role, got '{item.role.value}'."
                    )
                normalized.append(item)
            else:
                raise TypeError(f"Each response must be a string or ChatMessage, got {type(item)}.")
        return normalized

    def to_dict(self) -> dict[str, Any]:
        """Serialize the component to a dictionary."""
        responses = [msg.to_dict() for msg in self._responses] if self._responses is not None else None
        response_fn = serialize_callable(self.response_fn) if self.response_fn is not None else None
        streaming_callback = serialize_callable(self.streaming_callback) if self.streaming_callback else None
        return default_to_dict(
            self,
            responses=responses,
            response_fn=response_fn,
            model=self.model,
            meta=self.meta,

View on GitHub (pinned to e318778c9b)

Solutions

  1. Convert the message: ChatMessage.from_assistant(item.text) keeping the content you need
  2. Filter the list to assistant-role messages before constructing the mock
  3. Use strings in `responses` (they're auto-wrapped as assistant messages)

Example fix

// before
mock = MockChatGenerator(responses=[ChatMessage.from_user("hello")])
// after
mock = MockChatGenerator(responses=[ChatMessage.from_assistant("hello")])
Defensive patterns

Strategy: validation

Validate before calling

responses = [
    m if isinstance(m, str) or m.role == ChatRole.ASSISTANT
    else ChatMessage.from_assistant(m.text)
    for m in responses
]

Type guard

def is_assistant_message(m: ChatMessage) -> bool:
    return m.role == ChatRole.ASSISTANT

Prevention

When it happens

Trigger: `MockChatGenerator(responses=[ChatMessage.from_user("hi")])` or from_system/from_tool messages in the responses list — commonly when replaying a recorded conversation where user/system messages were included.

Common situations: Replaying captured transcripts without filtering roles; constructing messages with the wrong factory; copying ChatMessage objects from input logs into responses.

Related errors


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