2025-07-30 19:41:09 +08:00
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
fix(agent/tools): GoogleScholar empty json output and ignored top_n (#16419)
### What problem does this PR solve?
Closes #16418.
`scholarly.search_pubs(...)` returns a **lazy generator**, but
`agent/tools/googlescholar.py` treated it as a re-iterable, bounded
list:
```python
scholar_client = scholarly.search_pubs(kwargs["query"], ...) # lazy generator
self._retrieve_chunks(scholar_client, ...) # (1) iterates -> exhausts it
self.set_output("json", list(scholar_client)) # (2) already empty -> []
```
1. **`json` output was always empty.** `_retrieve_chunks` iterates
`scholar_client`, exhausting the generator; `list(scholar_client)` then
returns `[]`.
2. **`top_n` was never applied.** Unlike `ArXiv`
(`max_results=self._param.top_n`), the unbounded generator was passed
straight to `_retrieve_chunks`, which has no internal limit — so the
tool kept paginating well past Top N (until an error, rate-limit/block,
or `COMPONENT_EXEC_TIMEOUT`).
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
### Changes
- Materialize at most `top_n` results once with `itertools.islice`, and
reuse that list for both `_retrieve_chunks` and the `json` output.
- Add regression tests
(`test/unit_test/agent/component/test_googlescholar.py`, stubbing
`scholarly.search_pubs`) covering the `top_n` bound, the non-empty
`json` output, and the empty-query short-circuit.
Verified: against `main` the new tests fail with `assert 30 == 5` (top_n
ignored) and `assert 0 == 5` (empty json); with this fix all pass.
Backend-only.
---------
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-01 07:47:39 +05:00
import itertools
2025-07-30 19:41:09 +08:00
import logging
import os
import time
from abc import ABC
from scholarly import scholarly
from agent . tools . base import ToolMeta , ToolParamBase , ToolBase
2025-11-04 11:51:12 +08:00
from common . connection_utils import timeout
2025-07-30 19:41:09 +08:00
class GoogleScholarParam ( ToolParamBase ) :
"""
Define the GoogleScholar component parameters .
"""
def __init__ ( self ) :
2026-07-03 12:53:39 +08:00
self . meta : ToolMeta = {
2025-07-30 19:41:09 +08:00
" name " : " google_scholar_search " ,
" description " : """ Google Scholar provides a simple way to broadly search for scholarly literature. From one place, you can search across many disciplines and sources: articles, theses, books, abstracts and court opinions, from academic publishers, professional societies, online repositories, universities and other web sites. Google Scholar helps you find relevant work across the world of scholarly research. """ ,
" parameters " : {
" query " : {
" type " : " string " ,
" description " : " The search keyword to execute with Google Scholar. The keywords should be the most important words/terms(includes synonyms) from the original request. " ,
" default " : " {sys.query} " ,
2026-07-03 12:53:39 +08:00
" required " : True ,
2025-07-30 19:41:09 +08:00
}
2026-07-03 12:53:39 +08:00
} ,
2025-07-30 19:41:09 +08:00
}
super ( ) . __init__ ( )
self . top_n = 12
2026-07-03 12:53:39 +08:00
self . sort_by = " relevance "
2025-07-30 19:41:09 +08:00
self . year_low = None
self . year_high = None
self . patents = True
def check ( self ) :
self . check_positive_integer ( self . top_n , " Top N " )
2026-07-03 12:53:39 +08:00
self . check_valid_value ( self . sort_by , " GoogleScholar Sort_by " , [ " date " , " relevance " ] )
2025-07-30 19:41:09 +08:00
self . check_boolean ( self . patents , " Whether or not to include patents, defaults to True " )
def get_input_form ( self ) - > dict [ str , dict ] :
2026-07-03 12:53:39 +08:00
return { " query " : { " name " : " Query " , " type " : " line " } }
2025-07-30 19:41:09 +08:00
class GoogleScholar ( ToolBase , ABC ) :
component_name = " GoogleScholar "
2025-09-25 14:11:09 +08:00
@timeout ( int ( os . environ . get ( " COMPONENT_EXEC_TIMEOUT " , 12 ) ) )
2025-07-30 19:41:09 +08:00
def _invoke ( self , * * kwargs ) :
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " GoogleScholar processing " ) :
return
2025-07-30 19:41:09 +08:00
if not kwargs . get ( " query " ) :
self . set_output ( " formalized_content " , " " )
fix(agent/tools): GoogleScholar empty json output and ignored top_n (#16419)
### What problem does this PR solve?
Closes #16418.
`scholarly.search_pubs(...)` returns a **lazy generator**, but
`agent/tools/googlescholar.py` treated it as a re-iterable, bounded
list:
```python
scholar_client = scholarly.search_pubs(kwargs["query"], ...) # lazy generator
self._retrieve_chunks(scholar_client, ...) # (1) iterates -> exhausts it
self.set_output("json", list(scholar_client)) # (2) already empty -> []
```
1. **`json` output was always empty.** `_retrieve_chunks` iterates
`scholar_client`, exhausting the generator; `list(scholar_client)` then
returns `[]`.
2. **`top_n` was never applied.** Unlike `ArXiv`
(`max_results=self._param.top_n`), the unbounded generator was passed
straight to `_retrieve_chunks`, which has no internal limit — so the
tool kept paginating well past Top N (until an error, rate-limit/block,
or `COMPONENT_EXEC_TIMEOUT`).
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
### Changes
- Materialize at most `top_n` results once with `itertools.islice`, and
reuse that list for both `_retrieve_chunks` and the `json` output.
- Add regression tests
(`test/unit_test/agent/component/test_googlescholar.py`, stubbing
`scholarly.search_pubs`) covering the `top_n` bound, the non-empty
`json` output, and the empty-query short-circuit.
Verified: against `main` the new tests fail with `assert 30 == 5` (top_n
ignored) and `assert 0 == 5` (empty json); with this fix all pass.
Backend-only.
---------
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-01 07:47:39 +05:00
# Reset json too, otherwise a reused instance keeps stale results
# from a previous successful call.
self . set_output ( " json " , [ ] )
2025-07-30 19:41:09 +08:00
return " "
last_e = " "
2026-07-03 12:53:39 +08:00
for _ in range ( self . _param . max_retries + 1 ) :
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " GoogleScholar processing " ) :
return
2025-07-30 19:41:09 +08:00
try :
2026-07-03 12:53:39 +08:00
scholar_client = scholarly . search_pubs ( kwargs [ " query " ] , patents = self . _param . patents , year_low = self . _param . year_low , year_high = self . _param . year_high , sort_by = self . _param . sort_by )
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " GoogleScholar processing " ) :
return
fix(agent/tools): GoogleScholar empty json output and ignored top_n (#16419)
### What problem does this PR solve?
Closes #16418.
`scholarly.search_pubs(...)` returns a **lazy generator**, but
`agent/tools/googlescholar.py` treated it as a re-iterable, bounded
list:
```python
scholar_client = scholarly.search_pubs(kwargs["query"], ...) # lazy generator
self._retrieve_chunks(scholar_client, ...) # (1) iterates -> exhausts it
self.set_output("json", list(scholar_client)) # (2) already empty -> []
```
1. **`json` output was always empty.** `_retrieve_chunks` iterates
`scholar_client`, exhausting the generator; `list(scholar_client)` then
returns `[]`.
2. **`top_n` was never applied.** Unlike `ArXiv`
(`max_results=self._param.top_n`), the unbounded generator was passed
straight to `_retrieve_chunks`, which has no internal limit — so the
tool kept paginating well past Top N (until an error, rate-limit/block,
or `COMPONENT_EXEC_TIMEOUT`).
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
### Changes
- Materialize at most `top_n` results once with `itertools.islice`, and
reuse that list for both `_retrieve_chunks` and the `json` output.
- Add regression tests
(`test/unit_test/agent/component/test_googlescholar.py`, stubbing
`scholarly.search_pubs`) covering the `top_n` bound, the non-empty
`json` output, and the empty-query short-circuit.
Verified: against `main` the new tests fail with `assert 30 == 5` (top_n
ignored) and `assert 0 == 5` (empty json); with this fix all pass.
Backend-only.
---------
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-01 07:47:39 +05:00
# search_pubs returns a lazy generator: materialize at most top_n
# results once so the bound is respected and the same list feeds
# both _retrieve_chunks and the json output (iterating it twice
# would otherwise leave json empty).
results = list ( itertools . islice ( scholar_client , self . _param . top_n ) )
2026-07-03 12:53:39 +08:00
self . _retrieve_chunks (
results ,
get_title = lambda r : r [ " bib " ] [ " title " ] ,
get_url = lambda r : r [ " pub_url " ] ,
get_content = lambda r : " \n author: " + " , " . join ( r [ " bib " ] [ " author " ] ) + " \n Abstract: " + r [ " bib " ] . get ( " abstract " , " no abstract " ) ,
)
fix(agent/tools): GoogleScholar empty json output and ignored top_n (#16419)
### What problem does this PR solve?
Closes #16418.
`scholarly.search_pubs(...)` returns a **lazy generator**, but
`agent/tools/googlescholar.py` treated it as a re-iterable, bounded
list:
```python
scholar_client = scholarly.search_pubs(kwargs["query"], ...) # lazy generator
self._retrieve_chunks(scholar_client, ...) # (1) iterates -> exhausts it
self.set_output("json", list(scholar_client)) # (2) already empty -> []
```
1. **`json` output was always empty.** `_retrieve_chunks` iterates
`scholar_client`, exhausting the generator; `list(scholar_client)` then
returns `[]`.
2. **`top_n` was never applied.** Unlike `ArXiv`
(`max_results=self._param.top_n`), the unbounded generator was passed
straight to `_retrieve_chunks`, which has no internal limit — so the
tool kept paginating well past Top N (until an error, rate-limit/block,
or `COMPONENT_EXEC_TIMEOUT`).
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
### Changes
- Materialize at most `top_n` results once with `itertools.islice`, and
reuse that list for both `_retrieve_chunks` and the `json` output.
- Add regression tests
(`test/unit_test/agent/component/test_googlescholar.py`, stubbing
`scholarly.search_pubs`) covering the `top_n` bound, the non-empty
`json` output, and the empty-query short-circuit.
Verified: against `main` the new tests fail with `assert 30 == 5` (top_n
ignored) and `assert 0 == 5` (empty json); with this fix all pass.
Backend-only.
---------
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-07-01 07:47:39 +05:00
self . set_output ( " json " , results )
2025-07-30 19:41:09 +08:00
return self . output ( " formalized_content " )
except Exception as e :
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " GoogleScholar processing " ) :
return
2025-07-30 19:41:09 +08:00
last_e = e
logging . exception ( f " GoogleScholar error: { e } " )
time . sleep ( self . _param . delay_after_error )
if last_e :
self . set_output ( " _ERROR " , str ( last_e ) )
return f " GoogleScholar error: { last_e } "
assert False , self . output ( )
2025-07-31 15:13:45 +08:00
def thoughts ( self ) - > str :
2025-09-25 14:11:09 +08:00
return " Looking for scholarly papers on ` {} `,” prioritising reputable sources. " . format ( self . get_input ( ) . get ( " query " , " -_-! " ) )