def answer(self, qa: BaseQaData, question_idx: int) -> Dict:
        """Decompose the question in the given qa step-by-step and give the answer to it in the end.

        Before give the answer to the question, there would be a decompose-retrieve-select loop to collect the useful
        atom information. In every loop, there are three steps:
        - Step 1: Proposal. Decompose the question given the atom information we already have. The output of step 1
            would be a list of sub-questions that may be useful to answer the final question;
        - Step 2: Retrieval. Retrieve the relevant atom information (including the atoms and source chunks) from vector
            stores with the sub-questions list we got in step 1 and the final question we are going to answer. The
            output of step 2 would be a list of atom information candidates.
        - Step 3: Selection. Select the most useful atom information from the given candidates, based on the final
            question to be answered and the atom information we already have. The output of step 3 would be the chosen
            atom information, if any.

        The final step out of the loop is to let the LLM answer the original question given all the chosen atom
        information.
        """
        decomposition_infos: dict = {}
        chosen_atom_infos: List[AtomRetrievalInfo] = []
        while len(chosen_atom_infos) < self._max_num_question:
            sub_question_id: str = f"Sub{len(chosen_atom_infos) + 1}"
            decomposition_infos[sub_question_id] = {}

            # Step 1: Let LLM client provide a decomposition proposal with current context.
            decompose, thinking, proposals = self._propose_question_decomposition(qa.question, chosen_atom_infos)
            decomposition_infos[sub_question_id]["proposal"] = {
                "to_decompose": decompose,
                "thinking": thinking,
                "proposal_list": proposals,
            }
            if not decompose:
                break

            # Step 2: Retrieve relevant atom information to the sub-question proposals.
            atom_info_candidates = self._retrieve_atom_info_candidates(
                atom_queries=proposals,
                query=qa.question,
                chosen_atom_infos=chosen_atom_infos,
                retrieve_id=sub_question_id,
            )
            decomposition_infos[sub_question_id]["retrieval"] = [
                {
                    "relevant_proposal": info.atom_query,
                    "sub-question": info.atom,
                    "relevant_context_title": info.source_chunk_title,
                    "relevant_context": info.source_chunk,
                }
                for info in atom_info_candidates
            ]
            if len(atom_info_candidates) == 0:
                break

            # Step 3: Let LLM client select following sub-question from the candidates with current context.
            selected, thinking, chosen_info = self._select_atom_question(
                qa.question,
                atom_info_candidates,
                chosen_atom_infos,
            )
            decomposition_infos[sub_question_id]["selection"] = {
                "selected": selected,
                "thinking": thinking,
            }
            if selected:
                chosen_atom_infos.append(chosen_info)
                decomposition_infos[sub_question_id]["selection"]["chosen_info"] = {
                    "question": chosen_info.atom,
                    "source_chunk_title": chosen_info.source_chunk_title,
                    "source_chunk": chosen_info.source_chunk,
                }
            else:
                # TODO: re-propose?
                break

        # Last Step: Let LLM client answer the original question with all chosen atom information during the loop above.
        output = self._answer_original_question(qa.question, chosen_atom_infos)
        output["decomposition_infos"] = decomposition_infos
        return output
Decompose the question in the given qa step-by-step and give the answer to it in the end.

        Before give the answer to the question, there would be a decompose-retrieve-select loop to collect the useful
        atom information. In every loop, there are three steps:
        - Step 1: Proposal. Decompose the question given the atom information we already have. The output of step 1
            would be a list of sub-questions that may be useful to answer the final question;
        - Step 2: Retrieval. Retrieve the relevant atom information (including the atoms and source chunks) from vector
            stores with the sub-questions list we got in step 1 and the final question we are going to answer. The
            output of step 2 would be a list of atom information candidates.
        - Step 3: Selection. Select the most useful atom information from the given candidates, based on the final
            question to be answered and the atom information we already have. The output of step 3 would be the chosen
            atom information, if any.

        The final step out of the loop is to let the LLM answer the original question given all the chosen atom
        information.
        
pikerag/workflows/qa_decompose.py