[Bug]: TreeSelectLeafRetriever and tree insert silently pick the last node on 0-indexed LLM answers
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_leveland._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).
Source: run-llama/llama_index