#23076·llama_index

[Bug]: TreeSelectLeafRetriever and tree insert silently pick the last node on 0-indexed LLM answers

Author: Harsh23KashyapCreated Sep 16, 2026Updated Sep 16, 2026

Bug Description

The tree-select query prompt enumerates choices as 1 to N and asks the model to reply ANSWER: <number>. Both consumers of that answer only check the upper bound before indexing with number - 1:

  • TreeSelectLeafRetriever._query_level and ._select_nodes (indices/tree/select_leaf_retriever.py): if number > len(cur_node_list) guards over-range answers, but nothing guards 0.
  • TreeIndexInserter._insert_node (indices/tree/inserter.py): same shape, elif int(numbers[0]) > len(cur_graph_node_list).

A 0-indexed answer (a common model slip) passes both checks, number - 1 becomes -1, and Python's negative indexing silently selects the last node:

  • Retrieval descends into the last subtree and returns its leaves as if the model chose them (see Steps to Reproduce).
  • tree.insert(doc) files the new document under the last summary node instead of following the fallback path that over-range and unparseable answers take.

This is the same failure mode as #23072 (selectors) and #22827 (StructuredLLMRerank), in the tree index. Proposed fix: extend the existing out-of-range handling to number < 1 in all three places - the retriever bails out exactly as it already does for over-range answers, and insert falls back to inserting under the parent. I have the patch and regression tests ready - happy to send the PR.

Version

llama-index-core 0.14.24 (also verified on current main, fd4a517)

Steps to Reproduce

from llama_index.core.base.llms.types import CompletionResponse, LLMMetadata
from llama_index.core.indices.query.schema import QueryBundle
from llama_index.core.indices.tree.base import TreeIndex
from llama_index.core.llms.custom import CustomLLM
from llama_index.core.schema import Document


class ZeroIndexedLLM(CustomLLM):
    """Answers 0 (0-indexed) to every tree-select prompt."""

    @property
    def metadata(self) -> LLMMetadata:
        return LLMMetadata(context_window=4096, num_output=256)

    def complete(self, prompt, formatted=False, **kwargs):
        if "Some choices are given below" in prompt:
            return CompletionResponse(text="ANSWER: 0")
        return CompletionResponse(text="summary")

    def stream_complete(self, prompt, formatted=False, **kwargs):
        raise NotImplementedError


docs = [Document(text=f"This is doc {i}.") for i in range(4)]
tree = TreeIndex.from_documents(docs, llm=ZeroIndexedLLM(), num_children=2)

response = tree.as_retriever()._query(QueryBundle("What is?"))
print([n.node.get_content() for n in response.source_nodes])

Output: ['This is doc 3.'] - the model answered 0, but the retriever silently returned the leaf of the LAST subtree. The over-range path in the same function treats this as invalid and bails out; 0 should get the same treatment (the choice list starts at 1).