feat(robomp): added repo-scoped issue search to issue triage flow

- Added `search_issues` support to the GitHub backend and client, including `state_reason` and `is_pull_request` in issue summaries.
- Added the `gh_search_issues` host tool with repo-prefixed query handling, non-empty/restricted `repo:` validation, default and bounded `limit` values, and inbound issue filtering.
- Added proxy integration for issue search with a new `/gh/v1/search_issues` endpoint and matching proxy-client method/response parsing.
- Updated triage prompts to perform pre-`classify_issue` duplicate and already-fixed checks via search, and added tests for search query formatting, validation, and state-aware match rendering.
This commit is contained in:
can1357
2026-07-15 00:30:15 +02:00
parent 539c132094
commit 3e6ae36c7c
12 changed files with 257 additions and 19 deletions
+42 -16
View File
@@ -116,6 +116,10 @@ class IssueSummary:
updated_at: str
created_at: str
html_url: str
# `completed` / `not_planned` / `reopened` when closed; empty otherwise.
state_reason: str = ""
# Search results mix issues and PRs; list_issues always yields issues.
is_pull_request: bool = False
@dataclass(slots=True, frozen=True)
@@ -335,24 +339,26 @@ class GitHubClient:
for item in data or []:
if "pull_request" in item:
continue # GitHub's /issues endpoint also returns PRs; skip them.
user = item.get("user") or {}
labels_raw = item.get("labels") or []
out.append(
IssueSummary(
repo=repo,
number=int(item["number"]),
title=str(item.get("title") or ""),
state=str(item.get("state") or "open"),
author=str(user.get("login") or ""),
labels=tuple(str(lbl["name"]) if isinstance(lbl, dict) else str(lbl) for lbl in labels_raw),
comments=int(item.get("comments") or 0),
updated_at=str(item.get("updated_at") or ""),
created_at=str(item.get("created_at") or ""),
html_url=str(item.get("html_url") or ""),
)
)
out.append(_summary_from_item(repo, item))
return out
async def search_issues(self, repo: str, query: str, *, limit: int = 10) -> list[IssueSummary]:
"""Search issues AND pull requests in `repo` using GitHub issue-search syntax.
`query` takes bare keywords plus qualifiers (`is:pr`, `is:closed`,
`label:bug`, `in:title`, …); the `repo:` scope is applied here. Results
come back in GitHub's best-match order. `limit` is capped at 30 — this
serves triage lookups (duplicates, prior fixes), not pagination.
"""
per_page = max(1, min(int(limit), 30))
data = await self.request(
"GET",
"/search/issues",
params={"q": f"repo:{repo} {query}".strip(), "per_page": per_page},
)
items = (data or {}).get("items") or []
return [_summary_from_item(repo, item) for item in items]
async def list_comments(self, repo: str, number: int) -> list[CommentInfo]:
data = await self.request("GET", f"/repos/{repo}/issues/{number}/comments", params={"per_page": 100})
return [_comment_from_payload(item) for item in (data or [])]
@@ -576,6 +582,26 @@ def _pr_review_from_payload(data: Mapping[str, Any]) -> PullRequestReviewInfo:
)
def _summary_from_item(repo: str, item: Mapping[str, Any]) -> IssueSummary:
"""Build an `IssueSummary` from a REST issue object (list or search shape)."""
user = item.get("user") or {}
labels_raw = item.get("labels") or []
return IssueSummary(
repo=repo,
number=int(item["number"]),
title=str(item.get("title") or ""),
state=str(item.get("state") or "open"),
author=str(user.get("login") or ""),
labels=tuple(str(lbl["name"]) if isinstance(lbl, dict) else str(lbl) for lbl in labels_raw),
comments=int(item.get("comments") or 0),
updated_at=str(item.get("updated_at") or ""),
created_at=str(item.get("created_at") or ""),
html_url=str(item.get("html_url") or ""),
state_reason=str(item.get("state_reason") or ""),
is_pull_request="pull_request" in item,
)
def _pr_file_from_payload(data: Mapping[str, Any]) -> PullRequestFileInfo:
return PullRequestFileInfo(
path=str(data.get("filename") or data.get("path") or ""),