deepset-ai/haystack · error · ValueError

'response_fn' must return an assistant ChatMessage, got '{re

Error message

'response_fn' must return an assistant ChatMessage, got '{result.role.value}'.

What it means

MockChatGenerator wraps a user-supplied `response_fn` whose return value is coerced into a ChatMessage. If the function returns a ChatMessage whose role is not ASSISTANT, haystack raises ValueError because a generator reply must come from the assistant role for downstream pipeline consumers.

Source

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

        Return True if `response_fn` can be called as `response_fn(messages, tools)`.

        Callables that accept a single positional argument are called as `response_fn(messages)` instead.
        """
        try:
            inspect.signature(response_fn).bind(None, None)
        except (TypeError, ValueError):
            # The callable rejects a second positional argument, or exposes no signature at all (some C callables).
            return False
        return True

    @staticmethod
    def _coerce_to_message(result: str | ChatMessage) -> ChatMessage:
        """Turn the output of `response_fn` into a `ChatMessage`, wrapping strings and requiring the assistant role."""
        if isinstance(result, str):
            return ChatMessage.from_assistant(result)
        if isinstance(result, ChatMessage):
            if result.role != ChatRole.ASSISTANT:
                raise ValueError(f"'response_fn' must return an assistant ChatMessage, got '{result.role.value}'.")
            return result
        raise TypeError(f"'response_fn' must return a string or ChatMessage, got {type(result)}.")

    @staticmethod
    def _estimate_usage(messages: list[ChatMessage], reply: ChatMessage) -> dict[str, int]:
        """
        Roughly estimate token usage as whitespace-separated word counts.

        This is an approximation (not real tokenization) intended to give downstream code realistic-looking metadata.
        """
        prompt_tokens = sum(len((message.text or "").split()) for message in messages)
        completion_tokens = len((reply.text or "").split())
        return {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
        }

View on GitHub (pinned to e318778c9b)

Solutions

  1. Change response_fn to return ChatMessage.from_assistant(text) instead of from_user/from_tool
  2. If the recorded message has a non-assistant role, rebuild it: ChatMessage.from_assistant(message.text)
  3. Return a plain str, which _coerce_to_message automatically wraps with the assistant role

Example fix

// before
def response_fn(messages):
    return ChatMessage.from_user("hello")
// after
def response_fn(messages):
    return ChatMessage.from_assistant("hello")
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_response(r):
    return isinstance(r, str) or (isinstance(r, ChatMessage) and r.role == ChatRole.ASSISTANT)
assert is_valid_response(response_fn(messages))

Type guard

def is_assistant_message(r) -> bool:
    return isinstance(r, ChatMessage) and r.role == ChatRole.ASSISTANT

Try / catch

try:
    gen = MockChatGenerator(response_fn=response_fn)
    gen.run([msg])
except ValueError as e:
    if "response_fn" in str(e):
        response_fn = lambda m: ChatMessage.from_assistant(str(m[-1].text))

Prevention

When it happens

Trigger: Passing `response_fn` to MockChatGenerator that returns a ChatMessage built with e.g. ChatMessage.from_user(...) or from_tool(...), or mutating a message's role before returning it.

Common situations: Developers replaying recorded messages (tool/user role) as canned responses, or building messages from logs where the stored role is 'user' or 'tool'.

Related errors


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