zylon-ai/private-gpt · error · ValueError

No user messages found in the chat history.

Error message

No user messages found in the chat history.

What it means

ValueError from ConversationCondenser._split_conversation. It scans chat_history from the end backwards for the last message with role 'user' and splits into (left, last_user, right); if no message has role 'user' it cannot split and raises. It is the prerequisite invariant for condensing: the last user message is always preserved.

Source

Thrown at private_gpt/components/chat/processors/chat_history/memory/strategies/condenser.py:82

        self.message_to_input = message_to_input
        self.builder = summarize_workflow_builder or get_global_injector().get(
            SummarizeWorkflowBuilder
        )
        self.llm_component = llm_component or get_global_injector().get(LLMComponent)
        self.prompt_builder_service = (
            prompt_builder_service or get_global_injector().get(PromptBuilderService)
        )

    def _split_conversation(
        self,
        chat_history: list[ChatMessage],
    ) -> tuple[list[ChatMessage], ChatMessage, list[ChatMessage]]:
        """Split conversation into left and right parts."""
        for i in range(len(chat_history) - 1, -1, -1):
            if chat_history[i].role == "user":
                return chat_history[:i], chat_history[i], chat_history[i + 1 :]

        raise ValueError("No user messages found in the chat history.")

    async def _get_messages_tokens(
        self,
        messages: ChatMessage | list[ChatMessage],
        tokenizer_fn: TokenizerFn | None = None,
    ) -> int:
        if not messages:
            return 0
        if isinstance(messages, ChatMessage):
            messages = [messages]

        return await estimate_token_count(
            messages,
            tokenizer_fn=tokenizer_fn,
            message_to_input=self.message_to_input,
        )

    async def _summarize(

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Ensure the chat history passed to condensing contains at least one ChatMessage with role exactly 'user'.
  2. Normalize role names when constructing history (map 'human' -> 'user') before invoking the condenser.
  3. Skip condensing for histories with no user turn (they are already assistant-only; nothing to preserve).
  4. In tests, always append a user message to fixtures.

Example fix

// before
history = [ChatMessage(role="assistant", content="hi")]
condensed = await condenser.condense(history)  # ValueError

// after
history = [ChatMessage(role="user", content="hi")]
condensed = await condenser.condense(history)
Defensive patterns

Strategy: validation

Validate before calling

if not any(m.role == "user" for m in chat_history):
    # nothing to preserve; skip condensing entirely
    return chat_history

Type guard

def history_has_user(messages: list[ChatMessage]) -> bool:
    return any(m.role == "user" for m in messages)

Try / catch

try:
    condensed = await condenser.condense(history)
except ValueError as e:
    if "No user messages" in str(e):
        return history  # assistant-only history, return as-is
    raise

Prevention

When it happens

Trigger: Invoking the condenser on a history consisting only of assistant/system/tool messages; a degenerate single-message history with role != 'user'; tests constructing histories without a user turn; upstream steps dropping or reassigning the user role.

Common situations: Programmatic chat pipelines that seed history with prior assistant output; role mapped to 'human' or 'USER' (case/alias mismatch) when building ChatMessage objects; empty conversations guarded incorrectly.

Related errors


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