Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions packages/leann-core/src/leann/react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,20 +283,6 @@ def run(self, question: str, top_k: int = 5) -> str:
}
)

if results_count == 0 and iteration >= 2:
logger.warning("No results found, asking LLM for final answer...")
final_prompt = f"""Based on the previous searches, provide your best answer to the question.

Question: {question}

Previous searches and results:
{chr(10).join(all_context)}

Since no new results were found, provide your final answer based on what you know.
"""
final_answer = self.llm.ask(final_prompt)
return final_answer.strip()

logger.warning(f"Reached max iterations ({self.max_iterations}), getting final answer...")
final_prompt = f"""Based on all the searches performed, provide your final answer to the question.

Expand Down
99 changes: 99 additions & 0 deletions tests/test_react_dual_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,102 @@ def test_search_history_has_source_field():

assert "source" in agent.search_history[0]
assert agent.search_history[0]["source"] in ("local", "web")


def test_zero_results_on_iteration_two_does_not_abort_early():
"""A single empty tool result must not force an ungrounded final answer (#381)."""
searcher = _make_searcher()
searcher.search.side_effect = [
[
SearchResult(
id="1",
score=0.9,
text="Partial LEANN benchmark mention.",
metadata={"source": "docs"},
)
],
[],
[
SearchResult(
id="2",
score=0.95,
text="LEANN recall@10 = 0.95 vs FAISS 0.97 per the paper.",
metadata={"source": "web"},
)
],
]

mock_llm = MagicMock()
mock_llm.ask.side_effect = [
'Thought: Start local.\nAction: leann_search("LEANN benchmark")',
'Thought: Narrow local query.\nAction: leann_search("LEANN recall numbers vs faiss")',
'Thought: Try web.\nAction: web_search("LEANN paper recall comparison FAISS")',
"Thought: Found it.\nAction: Final Answer: LEANN achieves recall@10 = 0.95 vs FAISS 0.97.",
]

with patch.object(WebSearcher, "search") as mock_web:
mock_web.return_value = [
{
"title": "LEANN paper",
"link": "https://example.com/paper",
"snippet": "recall@10 comparison",
}
]
agent = ReActAgent(
searcher=searcher,
llm=mock_llm,
max_iterations=5,
serper_api_key="test-key",
)
answer = agent.run("What is LEANN's recall vs FAISS at recall@10?", top_k=2)

assert len(agent.search_history) == 3
assert agent.search_history[1]["results_count"] == 0
assert "0.95" in answer
assert mock_llm.ask.call_count == 4


def test_transient_web_failure_allows_retry_on_next_iteration():
"""Transient web_search failure should be retried, not terminate the loop (#381)."""
searcher = _make_searcher()
searcher.search.return_value = [
SearchResult(
id="1",
score=0.8,
text="Local setup notes.",
metadata={"source": "docs"},
)
]

mock_llm = MagicMock()
mock_llm.ask.side_effect = [
'Thought: Check local docs.\nAction: leann_search("project setup")',
'Thought: Check web.\nAction: web_search("latest setup guide")',
'Thought: Retry web.\nAction: web_search("setup guide tutorial")',
"Thought: Found it.\nAction: Final Answer: per the latest docs, run `make install`.",
]

with patch.object(WebSearcher, "search") as mock_web:
mock_web.side_effect = [
[{"title": "Error", "link": "", "snippet": "Web Search failed:502 Bad Gateway"}],
[
{
"title": "Setup guide",
"link": "https://example.com/setup",
"snippet": "Run `make install` to set up the project.",
}
],
]
agent = ReActAgent(
searcher=searcher,
llm=mock_llm,
max_iterations=5,
serper_api_key="test-key",
)
answer = agent.run("How do I set up the project?", top_k=2)

assert len(agent.search_history) == 3
assert agent.search_history[1]["results_count"] == 0
assert agent.search_history[2]["results_count"] == 1
assert "make install" in answer
assert mock_llm.ask.call_count == 4
Loading