zylon-ai/private-gpt · error · ValueError

No user message found in conversation history.

Error message

No user message found in conversation history.

What it means

build_condensed_history scans conversation_history in reverse for the last user message and refuses to proceed if none exists, since the condensed history format requires at least one user turn. This is an input-shape invariant: the conversation portion must contain a user message.

Solutions

  1. Ensure the input history contains at least one MessageRole.USER message before condensing
  2. Fix upstream classification so user messages are never routed into system_messages
  3. If assistant-first histories are legitimate, synthesize or skip condensation for them

Example fix

# before
condensed, blocks = build_condensed_history(system_messages, conversation_history)

# after
if not any(m.role == MessageRole.USER for m in conversation_history):
    conversation_history = [ChatMessage(role=MessageRole.USER, content="(continue)")] + conversation_history
condensed, blocks = build_condensed_history(system_messages, conversation_history)
Defensive patterns

Strategy: validation

Validate before calling

def has_user_message(conversation_history: list[ChatMessage]) -> bool:
    return any(m.role == MessageRole.USER for m in conversation_history)

assert has_user_message(conversation_history), "conversation must contain a user message"

Type guard

def is_condensable_history(history: list[ChatMessage]) -> bool:
    return any(m.role == MessageRole.USER for m in history)

Try / catch

try:
    condensed, blocks = build_condensed_history(system_messages, conversation_history)
except ValueError as e:
    if "No user message found" in str(e):
        conversation_history = [ChatMessage(role=MessageRole.USER, content="(continue)")] + conversation_history
        condensed, blocks = build_condensed_history(system_messages, conversation_history)
    else:
        raise

Prevention

When it happens

Trigger: Calling build_condensed_history(system_messages, conversation_history) where every message in conversation_history has role != MessageRole.USER (e.g. only assistant/system messages were classified as conversation).

Common situations: Upstream splitting logic (get_system_and_conversation_messages) misclassifying the only user message as a system message; history loaded from storage missing the user turn; a conversation that genuinely starts with an assistant greeting.

Related errors


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

Appendix: source

Thrown at private_gpt/components/chat/processors/chat_history/memory/utils/condenser.py:24

from private_gpt.components.chat.processors.chat_history.memory.utils.format import (
    guarantee_valid_message_sequence,
)
from private_gpt.events.models import BasicContentBlockType, TextBlock


def build_condensed_history(
    system_messages: list[ChatMessage],
    conversation_history: list[ChatMessage],
) -> tuple[list[ChatMessage], list[BasicContentBlockType]]:
    """Build the final condensed chat history."""
    last_user_message: ChatMessage | None = None
    for msg in reversed(conversation_history):
        if msg.role == MessageRole.USER:
            last_user_message = msg
            break

    if not last_user_message:
        raise ValueError("No user message found in conversation history.")

    def is_truncated(msg: ChatMessage) -> bool:
        if not msg.additional_kwargs:
            return False
        if "tldr" not in msg.additional_kwargs:
            return False
        tldr_value = msg.additional_kwargs["tldr"]
        return bool(tldr_value)

    truncated_blocks: list[BasicContentBlockType] = [
        TextBlock(
            text=messages_to_history_str([msg], show_index=False, show_role=False),
            metadata={
                "type": "tldr",
                "role": msg.role,
                "tldr_side": msg.additional_kwargs.get("tldr", "left")
                if isinstance(msg.additional_kwargs.get("tldr"), str)
                else "left",

View on GitHub (pinned to 4a030776a3)