Bug: failed chat search reuses metadata from the previous successful search
Describe the bug
ChatAgentWithMemory.process_chat_completion can associate a failed search tool call with metadata from an earlier successful search.
When a completion contains multiple search tool calls, the method re-executes each search sequentially to populate self.search_metadata.
A successful search replaces that state. However, if a subsequent search raises an exception, quick_search returns an error result without updating or clearing self.search_metadata.
For this sequence:
first query -> succeeds
second query -> raises an exceptionthe returned metadata for second query contains:
{
"query": "first query",
"sources": [
{
"title": "First source",
"url": "https://example.com/first",
"content": "First result",
},
],
}The failed search is therefore reported with the previous query and its sources.
To Reproduce
- Check out GPT Researcher
mainat commit:
6f998577d547b1e54ec662dac63583aa11e3b84b- Install the project and test dependencies:
python -m pip install -e ".[test]"- Create
tests/test_chat_search_metadata_failure.py:
from types import SimpleNamespace
import pytest
from backend.chat.chat import ChatAgentWithMemory
class SearchClient:
def __init__(self):
self.calls = 0
def search(self, *, query, max_results):
assert max_results == 5
self.calls += 1
if self.calls == 1:
return {
"results": [
{
"title": "First source",
"url": "https://example.com/first",
"content": "First result",
}
]
}
raise RuntimeError(
f"search failed for {query}"
)
@pytest.mark.asyncio
async def test_failed_second_search_does_not_reuse_first_metadata(
monkeypatch,
):
async def fake_completion(**_kwargs):
return "answer", [
{
"tool": "search_tool",
"args": {
"query": "first query",
},
},
{
"tool": "search_tool",
"args": {
"query": "second query",
},
},
]
monkeypatch.setattr(
"backend.chat.chat."
"create_chat_completion_with_tools",
fake_completion,
)
agent = ChatAgentWithMemory.__new__(
ChatAgentWithMemory
)
agent.config = SimpleNamespace(
smart_llm_model="test-model",
smart_llm_provider="test-provider",
llm_kwargs={},
)
agent.tavily_client = SearchClient()
agent.search_metadata = None
response, metadata = (
await agent.process_chat_completion([])
)
assert response == "answer"
assert (
metadata[0]["search_metadata"]["query"]
== "first query"
)
assert (
metadata[1]["search_metadata"]["query"]
== "second query"
)
assert (
metadata[1]["search_metadata"]["sources"]
== []
)
assert (
"error"
in metadata[1]["search_metadata"]
)- Run:
python -m pytest \
tests/test_chat_search_metadata_failure.py \
-q- Observe that the test fails because the second metadata entry still identifies
first query.
Expected behavior
Each search tool call should receive metadata describing that same search.
When second query fails, its metadata should identify the current query, contain no sources from an earlier search, and expose the current error:
{
"query": "second query",
"sources": [],
"error": "search failed for second query",
}The exact error representation may vary, but metadata from first query must not be reused.
Actual behavior
The first search succeeds and stores:
{
"query": "first query",
"sources": [
{
"title": "First source",
"url": "https://example.com/first",
"content": "First result",
},
],
}The second search raises:
RuntimeError: search failed for second queryHowever, the second returned tool metadata still contains the first search's state:
metadata[1] == {
"tool": "quick_search",
"query": "second query",
"search_metadata": {
"query": "first query",
"sources": [
{
"title": "First source",
"url": "https://example.com/first",
"content": "First result",
},
],
},
}Representative assertion:
AssertionError: assert 'first query' == 'second query'The failure reproduced consistently in two consecutive runs.
Screenshots
Not applicable. This is a deterministic chat-metadata reproduction without a browser, model, provider request, or live search request.
Desktop (please complete the following information):
- OS: macOS 15.7.3, arm64
- Browser: Not applicable
- GPT Researcher version: 0.14.7
- Revision:
main@6f998577d547b1e54ec662dac63583aa11e3b84b - Python: 3.12.13
- Installation: Source checkout
Smartphone (please complete the following information):
Not applicable.
Additional context
A successful search stores the current query and sources:
results = self.tavily_client.search(
query=query,
max_results=5,
)
self.search_metadata = {
"query": query,
"sources": [
...
],
}The exception branch returns an error but does not update that state:
except Exception as e:
logger.error(
f"Error performing web search: {str(e)}",
exc_info=True,
)
return {
"error": str(e),
"results": [],
}process_chat_completion then attaches the existing state to the current tool call:
if query:
self.quick_search(query)
processed_metadata.append({
"tool": "quick_search",
"query": query,
"search_metadata": self.search_metadata,
})Consequently, when the current search fails, self.search_metadata can still describe a previous successful search.
The no-client branch already demonstrates the expected state shape by storing the current query, an empty source list, and an error before returning:
self.search_metadata = {
"query": query,
"sources": [],
"error": (
"Web search is disabled - "
"TAVILY_API_KEY not configured"
),
}A possible fix is to apply the same state update in the exception branch:
except Exception as e:
error = str(e)
self.search_metadata = {
"query": query,
"sources": [],
"error": error,
}
return {
"error": error,
"results": [],
}Regression coverage should include:
- a single successful search;
- a single failed search;
- a successful search followed by a failed search;
- multiple successful searches;
- failures before any successful search;
- preservation of the current query in every metadata entry;
- absence of sources from previous searches after failure.
Targeted issue and pull-request searches found no existing report for this stale search-metadata root.
Source: assafelovic/gpt-researcher