From aff9ff6d0a34aab4dba715a1b4ce7de3ed585451 Mon Sep 17 00:00:00 2001 From: Yurii214 Date: Mon, 20 Jul 2026 11:53:53 +0200 Subject: [PATCH] fix(agent/tools): surface github search error responses (#17064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary `agent/tools/github.py` indexes `response["items"]` directly after `requests.get(...).json()`. the github search api returns `{"message": ...}` **without** an `items` key on realistic conditions — a rate limit (403/429; this tool sends no auth token, so the unauthenticated ~10 req/min search limit is easy to hit) or an invalid query (422). that raised `KeyError('items')`, which the tool's retry loop then surfaced to the model as the opaque `"GitHub error: 'items'"` instead of the real reason. this guards the missing key and raises the api's actual `message` (with a clear fallback) into the existing retry/`_ERROR` path, so the model sees e.g. `"GitHub error: API rate limit exceeded ..."`. valid responses are unchanged. adds `test/unit_test/agent/tools/test_github_unit.py` covering the rate-limit response (asserts the real message is surfaced, no `KeyError`) and the normal result path. Co-authored-by: Yaroslav98214 Co-authored-by: Haruko386 --- agent/tools/github.py | 11 ++- .../unit_test/agent/tools/test_github_unit.py | 78 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 test/unit_test/agent/tools/test_github_unit.py diff --git a/agent/tools/github.py b/agent/tools/github.py index 614e558d5b..022c6cf338 100644 --- a/agent/tools/github.py +++ b/agent/tools/github.py @@ -76,8 +76,15 @@ class GitHub(ToolBase, ABC): if self.check_if_canceled("GitHub processing"): return - self._retrieve_chunks(response["items"], get_title=lambda r: r["name"], get_url=lambda r: r["html_url"], get_content=lambda r: str(r["description"]) + "\n stars:" + str(r["watchers"])) - self.set_output("json", response["items"]) + # the github search api reports rate limits (403/429) and invalid + # queries (422) through a "message" field and omits "items"; surface + # that instead of raising a cryptic KeyError on the missing key. + if "items" not in response: + raise Exception(response.get("message", "GitHub search returned no items.")) + + items = response["items"] + self._retrieve_chunks(items, get_title=lambda r: r["name"], get_url=lambda r: r["html_url"], get_content=lambda r: str(r["description"]) + "\n stars:" + str(r["watchers"])) + self.set_output("json", items) return self.output("formalized_content") except Exception as e: if self.check_if_canceled("GitHub processing"): diff --git a/test/unit_test/agent/tools/test_github_unit.py b/test/unit_test/agent/tools/test_github_unit.py new file mode 100644 index 0000000000..c1840dabf6 --- /dev/null +++ b/test/unit_test/agent/tools/test_github_unit.py @@ -0,0 +1,78 @@ +# +# Copyright 2026 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. +# + +import agent.tools.github as gh_module +from agent.tools.github import GitHub, GitHubParam + + +class _FakeResp: + """Stands in for the requests.Response; json() returns a canned payload.""" + + def __init__(self, payload): + self._payload = payload + + def json(self): + return self._payload + + +def _make_tool(): + # Bypass the canvas-bound __init__ (mirrors test_googlescholar.py) and stub the + # canvas-touching helpers so we can exercise _invoke's response handling. Zero the + # error delay so the failing path doesn't sleep. + g = GitHub.__new__(GitHub) + param = GitHubParam() + param.top_n = 10 + param.max_retries = 0 + param.delay_after_error = 0 + g._param = param + g.check_if_canceled = lambda *a, **k: False + + captured = {} + out = {} + + def fake_retrieve(res_list, **_kw): + captured["chunks"] = list(res_list) + out["formalized_content"] = "FC" + + g._retrieve_chunks = fake_retrieve + g.set_output = lambda k, v: out.__setitem__(k, v) + g.output = lambda k=None: out.get(k) if k else out + return g, captured, out + + +def test_rate_limit_response_surfaces_message(monkeypatch): + # Regression: the github search api returns {"message": ...} with no "items" on a + # rate limit / invalid query. The tool used to raise KeyError('items'), reported to + # the model as the opaque "GitHub error: 'items'". It must surface the real message. + monkeypatch.setattr( + gh_module.requests, + "get", + lambda *a, **k: _FakeResp({"message": "API rate limit exceeded for x.", "documentation_url": "https://docs.github.com/rest"}), + ) + g, _, out = _make_tool() + result = g._invoke(query="anything") + assert "API rate limit exceeded for x." in result + assert "'items'" not in result + assert out.get("_ERROR") == "API rate limit exceeded for x." + + +def test_valid_response_returns_items(monkeypatch): + items = [{"name": "n", "html_url": "u", "description": "d", "watchers": 3}] + monkeypatch.setattr(gh_module.requests, "get", lambda *a, **k: _FakeResp({"items": items})) + g, captured, out = _make_tool() + g._invoke(query="anything") + assert captured["chunks"] == items + assert out["json"] == items