Fix RAG synthesis silently falling back to raw chunk dump

_query_base/_query_hybrid passed raw TextNodes to
get_response_synthesizer().synthesize(), which requires NodeWithScore
(it reads node.node internally). Every real query hit
AttributeError: 'TextNode' object has no attribute 'node', got caught
by a bare except Exception, and silently fell back to
_generate_fallback_answer - the "Based on the available
documentation: 1. From X... 2. From Y..." template users were
actually seeing instead of a real LLM-synthesized answer.

This was a pre-existing bug independent of the Groq change - Ollama
hit the same exception, it just wasn't noticed because the fallback
looks superficially like an answer.
This commit is contained in:
Rosie Val 2026-08-22 15:39:31 -04:00
parent 3512c615c9
commit 6a23cf80d5

View file

@ -15,7 +15,7 @@ from llama_index.core.tools import QueryEngineTool
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core.postprocessor import SentenceTransformerRerank, SimilarityPostprocessor
from llama_index.core.schema import TextNode
from llama_index.core.schema import TextNode, NodeWithScore
# BM25 retriever - needs to be installed separately
try:
@ -252,7 +252,7 @@ class RAGEngine:
# Synthesize answer
synthesizer = get_response_synthesizer()
response = synthesizer.synthesize(question, nodes=fused_nodes)
response = synthesizer.synthesize(question, nodes=self._to_scored_nodes(fused_nodes))
return self._format_response(response, fused_nodes)
@ -336,7 +336,7 @@ class RAGEngine:
try:
# Synthesize answer using LLM
synthesizer = get_response_synthesizer()
response = synthesizer.synthesize(question, nodes=filtered_nodes)
response = synthesizer.synthesize(question, nodes=self._to_scored_nodes(filtered_nodes))
logger.info(f"Generated answer: {str(response)[:200]}")
@ -369,6 +369,13 @@ class RAGEngine:
answer_parts.append(f"\n\nFor complete information, see the sources below.")
return "".join(answer_parts)
def _to_scored_nodes(self, nodes: List[TextNode]) -> List[NodeWithScore]:
"""Wrap raw TextNodes as NodeWithScore, which response synthesizers require."""
return [
NodeWithScore(node=node, score=(node.metadata or {}).get("similarity_score", 0.0))
for node in nodes
]
def _format_response(self, response, source_nodes) -> Dict[str, any]:
"""Format query response for return."""
sources = []