zylon-ai/private-gpt · error · ValueError

Message {j} in user block {i} is neither an assistant nor a

Error message

Message {j} in user block {i} is neither an assistant nor a tool: {message}

What it means

Interior user-block validation: every message strictly between the first and last of a block must be either 'assistant' or 'tool'. Any other role (in practice a second 'user' message inside the block) fails the check, because the block structure only admits user → (assistant|tool)* → end.

Source

Thrown at private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py:161

        # Validate the last message in the block
        if strict and block[-1].role != "assistant":
            raise ValueError(
                f"User block {i} does not end with an assistant message: {block[-1]}"
            )
        elif not strict and block[-1].role not in ["assistant", "tool"]:
            raise ValueError(
                f"User block {i} does not end with an assistant or tool message: {block[-1]}"
            )

        # Validate the tool messages
        for j, message in enumerate(block[1:-1]):
            if message.role == "tool":
                if j == 0 or block[j].role != "assistant":
                    raise ValueError(
                        f"Tool message {j} in user block {i} does not follow an assistant message: {message}"
                    )
            elif message.role != "assistant":
                raise ValueError(
                    f"Message {j} in user block {i} is neither an assistant nor a tool: {message}"
                )


def _assert_repair_without_tools(
    chat_history: list[ChatMessage],
) -> None:
    """Assert that the chat history does not contain any tool messages.

    This is used to ensure that the chat history is
    clean and does not contain any tool calls or tool messages.
    """
    if not chat_history:
        return

    def is_a_potential_tool_message(msg: ChatMessage) -> tuple[bool, str | None]:
        if msg.role == "tool":
            return True, "Tool message detected"

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Apply merge_adjacent_messages (and/or split into proper blocks) before calling repair_*
  2. Split the history at each user message so blocks contain only one user turn each
  3. Remove stray user messages that belong to another session

Example fix

# before
repaired = repair_without_tools(chat_history)

# after
from private_gpt.components.chat.processors.chat_history.memory.utils.merge import merge_adjacent_messages
repaired = repair_without_tools(merge_adjacent_messages(chat_history))
Defensive patterns

Strategy: validation

Validate before calling

def interior_is_assistant_or_tool(history: list[ChatMessage]) -> bool:
    for b in get_user_blocks(history):
        if any(m.role not in ("assistant", "tool") for m in b[1:-1]):
            return False
    return True

assert interior_is_assistant_or_tool(history)

Type guard

def has_clean_block_interiors(history: list[ChatMessage]) -> bool:
    return interior_is_assistant_or_tool(history)

Try / catch

try:
    repaired = repair_without_tools(chat_history)
except ValueError as e:
    if "neither an assistant nor a tool" in str(e):
        repaired = repair_without_tools(merge_adjacent_messages(chat_history))
    else:
        raise

Prevention

When it happens

Trigger: Calling repair_without_tools()/repair_with_tools() on history where a user message appears mid-block — i.e. two user turns with responses missing around them, or a user message sandwiched between assistant messages.

Common situations: Consecutive user messages not merged before validation; interleaved transcripts from multi-user sessions stuffed into one history; message ordering corrupted by a custom loader.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/3faf6053c006f71b. Report an issue: GitHub.