run-llama / llama_index

LlamaIndex is a data framework for your LLM applications
https://docs.llamaindex.ai
MIT License
33.23k stars 4.64k forks source link

[Bug]: #13859

Open botschmiede opened 1 month ago

botschmiede commented 1 month ago

Bug Description

RetrieverQueryEngine cant handle response_synthesizer objects when passing custom hybrid retriever following error message:

synthesizer object has no attribute 'metadata'

Where the synthesizer can be CompactAndRefine, Refine etc.

Version

Version: 0.10.22

Steps to Reproduce

def get_retriever_query_engine_for_cache():

    cache_bm25_retriever = get_BM25_retriever_for_cache()

    cache_vector_retriever = index_storage.index_cache().as_retriever(similarity_top_k=2)

    hybrid_retriever = HybridRetriever(cache_vector_retriever, cache_bm25_retriever)

    reranker = LLMRerank(
            choice_batch_size=20,
            top_n=2,
        )
    response_synthesizer = get_response_synthesizer(structured_answer_filtering=True)

    return RetrieverQueryEngine(
        retriever=hybrid_retriever, 
        response_synthesizer=get_response_synthesizer(response_synthesizer),
        node_postprocessors=[reranker,SimilarityPostprocessor(similarity_cutoff=SIMILARITY_CUTOFF_CACHE_CUSTOM_HYBRID)],
        )

The Custom Hybrid Retriever:

class HybridRetriever(BaseRetriever):
    def __init__(self, vector_retriever, bm25_retriever):
        self.vector_retriever = vector_retriever
        self.bm25_retriever = bm25_retriever
        super().__init__()

    def _retrieve(self, query, **kwargs):
        bm25_nodes = self.bm25_retriever.retrieve(query, **kwargs)
        vector_nodes = self.vector_retriever.retrieve(query, **kwargs)

        # combine the two lists of nodes
        all_nodes = []
        node_ids = set()
        for n in bm25_nodes + vector_nodes:
            if n.node.node_id not in node_ids:
                all_nodes.append(n)
                node_ids.add(n.node.node_id)
        return all_nodes

Relevant Logs/Tracbacks

No response

dosubot[bot] commented 1 month ago

@botschmiede Greetings! I'm here to assist you with any issues you're facing. Let's work together to resolve the bug you've encountered.

To resolve the synthesizer object has no attribute 'metadata' error when using RetrieverQueryEngine with a custom hybrid retriever and response_synthesizer objects like CompactAndRefine or Refine, you need to ensure that the RetrieverQueryEngine and the response_synthesizer are correctly set up and used.

Here is an example of how to set up and use the RetrieverQueryEngine and Refine class:

from collections import OrderedDict
from typing import Any, Dict, Optional, Type, cast

import pytest
from llama_index.legacy.bridge.pydantic import BaseModel
from llama_index.legacy.callbacks import CallbackManager
from llama_index.legacy.response_synthesizers import Refine
from llama_index.legacy.response_synthesizers.refine import (
    StructuredRefineResponse,
)
from llama_index.legacy.service_context import ServiceContext
from llama_index.legacy.types import BasePydanticProgram
from llama_index.core.query_engine.retriever_query_engine import RetrieverQueryEngine

class MockRefineProgram(BasePydanticProgram):
    def __init__(self, input_to_query_satisfied: Dict[str, bool]):
        self._input_to_query_satisfied = input_to_query_satisfied

    @property
    def output_cls(self) -> Type[BaseModel]:
        return StructuredRefineResponse

    def __call__(self, *args: Any, context_str: Optional[str] = None, context_msg: Optional[str] = None, **kwargs: Any) -> StructuredRefineResponse:
        input_str = context_str or context_msg
        input_str = cast(str, input_str)
        query_satisfied = self._input_to_query_satisfied[input_str]
        return StructuredRefineResponse(answer=input_str, query_satisfied=query_satisfied)

    async def acall(self, *args: Any, context_str: Optional[str] = None, context_msg: Optional[str] = None, **kwargs: Any) -> StructuredRefineResponse:
        input_str = context_str or context_msg
        input_str = cast(str, input_str)
        query_satisfied = self._input_to_query_satisfied[input_str]
        return StructuredRefineResponse(answer=input_str, query_satisfied=query_satisfied)

@pytest.fixture()
def mock_refine_service_context(patch_llm_predictor: Any) -> ServiceContext:
    cb_manager = CallbackManager([])
    return ServiceContext.from_defaults(llm_predictor=patch_llm_predictor, callback_manager=cb_manager)

@pytest.fixture()
def refine_instance(mock_refine_service_context: ServiceContext) -> Refine:
    return Refine(service_context=mock_refine_service_context, streaming=False, verbose=True, structured_answer_filtering=True)

@pytest.mark.asyncio()
async def test_answer_filtering_one_answer(mock_refine_service_context: ServiceContext) -> None:
    input_to_query_satisfied = OrderedDict([("input1", False), ("input2", True), ("input3", False)])

    def program_factory(*args: Any, **kwargs: Any) -> MockRefineProgram:
        return MockRefineProgram(input_to_query_satisfied)

    refine_instance = Refine(service_context=mock_refine_service_context, structured_answer_filtering=True, program_factory=program_factory)
    res = await refine_instance.aget_response("question", list(input_to_query_satisfied.keys()))
    assert res == "input2"

@pytest.mark.asyncio()
async def test_answer_filtering_no_answers(mock_refine_service_context: ServiceContext) -> None:
    input_to_query_satisfied = OrderedDict([("input1", False), ("input2", False), ("input3", False)])

    def program_factory(*args: Any, **kwargs: Any) -> MockRefineProgram:
        return MockRefineProgram(input_to_query_satisfied)

    refine_instance = Refine(service_context=mock_refine_service_context, structured_answer_filtering=True, program_factory=program_factory)
    res = await refine_instance.aget_response("question", list(input_to_query_satisfied.keys()))
    assert res == "Empty Response"

# Setting up the RetrieverQueryEngine
retriever = ...  # Your custom retriever
response_synthesizer = Refine(service_context=mock_refine_service_context, structured_answer_filtering=True, program_factory=program_factory)
query_engine = RetrieverQueryEngine(retriever=retriever, response_synthesizer=response_synthesizer)

# Example usage
query_bundle = ...  # Your query bundle
response = query_engine.synthesize(query_bundle, nodes)

Ensure that the RetrieverQueryEngine is correctly instantiated with the necessary parameters and that the Refine class is properly defined to handle the responses. This setup should help resolve the synthesizer object has no attribute 'metadata' error.

Additionally, the RetrieverQueryEngine class has a method synthesize that uses the response_synthesizer to generate the response. Make sure that the response_synthesizer is correctly initialized and passed to the RetrieverQueryEngine:

def synthesize(
    self,
    query_bundle: QueryBundle,
    nodes: List[NodeWithScore],
    additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
) -> RESPONSE_TYPE:
    return self._response_synthesizer.synthesize(
        query=query_bundle,
        nodes=nodes,
        additional_source_nodes=additional_source_nodes,
    )

This method should be used to synthesize the response from the retrieved nodes. Ensure that all necessary attributes and methods are correctly defined in your custom response_synthesizer to avoid attribute errors [1][2][3].

To continue talking to Dosu, mention @dosu.