chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from adk_stale_agent.settings import CLOSE_HOURS_AFTER_STALE_THRESHOLD
|
||||
from adk_stale_agent.settings import GITHUB_BASE_URL
|
||||
from adk_stale_agent.settings import GRAPHQL_COMMENT_LIMIT
|
||||
from adk_stale_agent.settings import GRAPHQL_EDIT_LIMIT
|
||||
from adk_stale_agent.settings import GRAPHQL_TIMELINE_LIMIT
|
||||
from adk_stale_agent.settings import LLM_MODEL_NAME
|
||||
from adk_stale_agent.settings import OWNER
|
||||
from adk_stale_agent.settings import REPO
|
||||
from adk_stale_agent.settings import REQUEST_CLARIFICATION_LABEL
|
||||
from adk_stale_agent.settings import STALE_HOURS_THRESHOLD
|
||||
from adk_stale_agent.settings import STALE_LABEL_NAME
|
||||
from adk_stale_agent.utils import delete_request
|
||||
from adk_stale_agent.utils import error_response
|
||||
from adk_stale_agent.utils import get_request
|
||||
from adk_stale_agent.utils import patch_request
|
||||
from adk_stale_agent.utils import post_request
|
||||
import dateutil.parser
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
logger = logging.getLogger("google_adk." + __name__)
|
||||
|
||||
# --- Constants ---
|
||||
# Used to detect if the bot has already posted an alert to avoid spamming.
|
||||
BOT_ALERT_SIGNATURE = (
|
||||
"**Notification:** The author has updated the issue description"
|
||||
)
|
||||
BOT_NAME = "adk-bot"
|
||||
|
||||
# --- Global Cache ---
|
||||
_MAINTAINERS_CACHE: Optional[List[str]] = None
|
||||
|
||||
|
||||
def _get_cached_maintainers() -> List[str]:
|
||||
"""
|
||||
Fetches the list of repository maintainers.
|
||||
|
||||
This function relies on `utils.get_request` for network resilience.
|
||||
`get_request` is configured with an HTTPAdapter that automatically performs
|
||||
exponential backoff retries (up to 6 times) for 5xx errors and rate limits.
|
||||
|
||||
If the retries are exhausted or the data format is invalid, this function
|
||||
raises a RuntimeError to prevent the bot from running with incorrect permissions.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of GitHub usernames with push access.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the API fails after all retries or returns invalid data.
|
||||
"""
|
||||
global _MAINTAINERS_CACHE
|
||||
if _MAINTAINERS_CACHE is not None:
|
||||
return _MAINTAINERS_CACHE
|
||||
|
||||
logger.info("Initializing Maintainers Cache...")
|
||||
|
||||
try:
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/collaborators"
|
||||
params = {"permission": "push"}
|
||||
|
||||
data = get_request(url, params)
|
||||
|
||||
if isinstance(data, list):
|
||||
_MAINTAINERS_CACHE = [u["login"] for u in data if "login" in u]
|
||||
logger.info(f"Cached {len(_MAINTAINERS_CACHE)} maintainers.")
|
||||
return _MAINTAINERS_CACHE
|
||||
else:
|
||||
logger.error(
|
||||
f"Invalid API response format: Expected list, got {type(data)}"
|
||||
)
|
||||
raise ValueError(f"GitHub API returned non-list data: {data}")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(
|
||||
f"FATAL: Failed to verify repository maintainers. Error: {e}"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Maintainer verification failed. processing aborted."
|
||||
) from e
|
||||
|
||||
|
||||
def load_prompt_template(filename: str) -> str:
|
||||
"""
|
||||
Loads the raw text content of a prompt file.
|
||||
|
||||
Args:
|
||||
filename (str): The name of the file (e.g., 'PROMPT_INSTRUCTION.txt').
|
||||
|
||||
Returns:
|
||||
str: The file content.
|
||||
"""
|
||||
file_path = os.path.join(os.path.dirname(__file__), filename)
|
||||
with open(file_path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
PROMPT_TEMPLATE = load_prompt_template("PROMPT_INSTRUCTION.txt")
|
||||
|
||||
|
||||
def _fetch_graphql_data(item_number: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Executes the GraphQL query to fetch raw issue data, including comments,
|
||||
edits, and timeline events.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The raw 'issue' object from the GraphQL response.
|
||||
|
||||
Raises:
|
||||
RequestException: If the GraphQL query returns errors or the issue is not found.
|
||||
"""
|
||||
query = """
|
||||
query($owner: String!, $name: String!, $number: Int!, $commentLimit: Int!, $timelineLimit: Int!, $editLimit: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
issue(number: $number) {
|
||||
author { login }
|
||||
createdAt
|
||||
labels(first: 20) { nodes { name } }
|
||||
|
||||
comments(last: $commentLimit) {
|
||||
nodes {
|
||||
author { login }
|
||||
body
|
||||
createdAt
|
||||
lastEditedAt
|
||||
}
|
||||
}
|
||||
|
||||
userContentEdits(last: $editLimit) {
|
||||
nodes {
|
||||
editor { login }
|
||||
editedAt
|
||||
}
|
||||
}
|
||||
|
||||
timelineItems(itemTypes: [LABELED_EVENT, RENAMED_TITLE_EVENT, REOPENED_EVENT], last: $timelineLimit) {
|
||||
nodes {
|
||||
__typename
|
||||
... on LabeledEvent {
|
||||
createdAt
|
||||
actor { login }
|
||||
label { name }
|
||||
}
|
||||
... on RenamedTitleEvent {
|
||||
createdAt
|
||||
actor { login }
|
||||
}
|
||||
... on ReopenedEvent {
|
||||
createdAt
|
||||
actor { login }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"owner": OWNER,
|
||||
"name": REPO,
|
||||
"number": item_number,
|
||||
"commentLimit": GRAPHQL_COMMENT_LIMIT,
|
||||
"editLimit": GRAPHQL_EDIT_LIMIT,
|
||||
"timelineLimit": GRAPHQL_TIMELINE_LIMIT,
|
||||
}
|
||||
|
||||
response = post_request(
|
||||
f"{GITHUB_BASE_URL}/graphql", {"query": query, "variables": variables}
|
||||
)
|
||||
|
||||
if "errors" in response:
|
||||
raise RequestException(f"GraphQL Error: {response['errors'][0]['message']}")
|
||||
|
||||
data = response.get("data", {}).get("repository", {}).get("issue", {})
|
||||
if not data:
|
||||
raise RequestException(f"Issue #{item_number} not found.")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _build_history_timeline(
|
||||
data: Dict[str, Any],
|
||||
) -> Tuple[List[Dict[str, Any]], List[datetime], Optional[datetime]]:
|
||||
"""
|
||||
Parses raw GraphQL data into a unified, chronologically sorted history list.
|
||||
Also extracts specific event times needed for logic checks.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): The raw issue data from `_fetch_graphql_data`.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Dict], List[datetime], Optional[datetime]]:
|
||||
- history: A list of normalized event dictionaries sorted by time.
|
||||
- label_events: A list of timestamps when the stale label was applied.
|
||||
- last_bot_alert_time: Timestamp of the last bot silent-edit alert (if any).
|
||||
"""
|
||||
issue_author = data.get("author", {}).get("login")
|
||||
history = []
|
||||
label_events = []
|
||||
last_bot_alert_time = None
|
||||
|
||||
# 1. Baseline: Issue Creation
|
||||
history.append({
|
||||
"type": "created",
|
||||
"actor": issue_author,
|
||||
"time": dateutil.parser.isoparse(data["createdAt"]),
|
||||
"data": None,
|
||||
})
|
||||
|
||||
# 2. Process Comments
|
||||
for c in data.get("comments", {}).get("nodes", []):
|
||||
if not c:
|
||||
continue
|
||||
|
||||
actor = c.get("author", {}).get("login")
|
||||
c_body = c.get("body", "")
|
||||
c_time = dateutil.parser.isoparse(c.get("createdAt"))
|
||||
|
||||
# Track bot alerts for spam prevention
|
||||
if BOT_ALERT_SIGNATURE in c_body:
|
||||
if last_bot_alert_time is None or c_time > last_bot_alert_time:
|
||||
last_bot_alert_time = c_time
|
||||
continue
|
||||
|
||||
if actor and not actor.endswith("[bot]") and actor != BOT_NAME:
|
||||
# Use edit time if available, otherwise creation time
|
||||
e_time = c.get("lastEditedAt")
|
||||
actual_time = dateutil.parser.isoparse(e_time) if e_time else c_time
|
||||
history.append({
|
||||
"type": "commented",
|
||||
"actor": actor,
|
||||
"time": actual_time,
|
||||
"data": c_body,
|
||||
})
|
||||
|
||||
# 3. Process Body Edits ("Ghost Edits")
|
||||
for e in data.get("userContentEdits", {}).get("nodes", []):
|
||||
if not e:
|
||||
continue
|
||||
actor = e.get("editor", {}).get("login")
|
||||
if actor and not actor.endswith("[bot]") and actor != BOT_NAME:
|
||||
history.append({
|
||||
"type": "edited_description",
|
||||
"actor": actor,
|
||||
"time": dateutil.parser.isoparse(e.get("editedAt")),
|
||||
"data": None,
|
||||
})
|
||||
|
||||
# 4. Process Timeline Events
|
||||
for t in data.get("timelineItems", {}).get("nodes", []):
|
||||
if not t:
|
||||
continue
|
||||
|
||||
etype = t.get("__typename")
|
||||
actor = t.get("actor", {}).get("login")
|
||||
time_val = dateutil.parser.isoparse(t.get("createdAt"))
|
||||
|
||||
if etype == "LabeledEvent":
|
||||
if t.get("label", {}).get("name") == STALE_LABEL_NAME:
|
||||
label_events.append(time_val)
|
||||
continue
|
||||
|
||||
if actor and not actor.endswith("[bot]") and actor != BOT_NAME:
|
||||
pretty_type = (
|
||||
"renamed_title" if etype == "RenamedTitleEvent" else "reopened"
|
||||
)
|
||||
history.append({
|
||||
"type": pretty_type,
|
||||
"actor": actor,
|
||||
"time": time_val,
|
||||
"data": None,
|
||||
})
|
||||
|
||||
# Sort chronologically
|
||||
history.sort(key=lambda x: x["time"])
|
||||
return history, label_events, last_bot_alert_time
|
||||
|
||||
|
||||
def _replay_history_to_find_state(
|
||||
history: List[Dict[str, Any]], maintainers: List[str], issue_author: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Replays the unified event history to determine the absolute last actor and their role.
|
||||
|
||||
Args:
|
||||
history (List[Dict]): Chronologically sorted list of events.
|
||||
maintainers (List[str]): List of maintainer usernames.
|
||||
issue_author (str): Username of the issue author.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary containing the last state of the issue:
|
||||
- last_action_role (str): 'author', 'maintainer', or 'other_user'.
|
||||
- last_activity_time (datetime): Timestamp of the last human action.
|
||||
- last_action_type (str): The type of the last action (e.g., 'commented').
|
||||
- last_comment_text (Optional[str]): The text of the last comment.
|
||||
- last_actor_name (str): The specific username of the last actor.
|
||||
"""
|
||||
last_action_role = "author"
|
||||
last_activity_time = history[0]["time"]
|
||||
last_action_type = "created"
|
||||
last_comment_text = None
|
||||
last_actor_name = issue_author
|
||||
|
||||
for event in history:
|
||||
actor = event["actor"]
|
||||
etype = event["type"]
|
||||
|
||||
role = "other_user"
|
||||
if actor == issue_author:
|
||||
role = "author"
|
||||
elif actor in maintainers:
|
||||
role = "maintainer"
|
||||
|
||||
last_action_role = role
|
||||
last_activity_time = event["time"]
|
||||
last_action_type = etype
|
||||
last_actor_name = actor
|
||||
|
||||
# Only store text if it was a comment (resets on other events like labels/edits)
|
||||
if etype == "commented":
|
||||
last_comment_text = event["data"]
|
||||
else:
|
||||
last_comment_text = None
|
||||
|
||||
return {
|
||||
"last_action_role": last_action_role,
|
||||
"last_activity_time": last_activity_time,
|
||||
"last_action_type": last_action_type,
|
||||
"last_comment_text": last_comment_text,
|
||||
"last_actor_name": last_actor_name,
|
||||
}
|
||||
|
||||
|
||||
def get_issue_state(item_number: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieves the comprehensive state of a GitHub issue using GraphQL.
|
||||
|
||||
This function orchestrates the fetching, parsing, and analysis of the issue's
|
||||
history to determine if it is stale, active, or pending maintainer review.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A comprehensive state dictionary for the LLM agent.
|
||||
Contains keys such as 'last_action_role', 'is_stale', 'days_since_activity',
|
||||
and 'maintainer_alert_needed'.
|
||||
"""
|
||||
try:
|
||||
maintainers = _get_cached_maintainers()
|
||||
|
||||
# 1. Fetch
|
||||
raw_data = _fetch_graphql_data(item_number)
|
||||
|
||||
issue_author = raw_data.get("author", {}).get("login")
|
||||
labels_list = [
|
||||
l["name"] for l in raw_data.get("labels", {}).get("nodes", [])
|
||||
]
|
||||
|
||||
# 2. Parse & Sort
|
||||
history, label_events, last_bot_alert_time = _build_history_timeline(
|
||||
raw_data
|
||||
)
|
||||
|
||||
# 3. Analyze (Replay)
|
||||
state = _replay_history_to_find_state(history, maintainers, issue_author)
|
||||
|
||||
# 4. Final Calculations & Alert Logic
|
||||
current_time = datetime.now(timezone.utc)
|
||||
days_since_activity = (
|
||||
current_time - state["last_activity_time"]
|
||||
).total_seconds() / 86400
|
||||
|
||||
# Stale Checks
|
||||
is_stale = STALE_LABEL_NAME in labels_list
|
||||
days_since_stale_label = 0.0
|
||||
if is_stale and label_events:
|
||||
latest_label_time = max(label_events)
|
||||
days_since_stale_label = (
|
||||
current_time - latest_label_time
|
||||
).total_seconds() / 86400
|
||||
|
||||
# Silent Edit Alert Logic
|
||||
maintainer_alert_needed = False
|
||||
if (
|
||||
state["last_action_role"] in ["author", "other_user"]
|
||||
and state["last_action_type"] == "edited_description"
|
||||
):
|
||||
if (
|
||||
last_bot_alert_time
|
||||
and last_bot_alert_time > state["last_activity_time"]
|
||||
):
|
||||
logger.info(
|
||||
f"#{item_number}: Silent edit detected, but Bot already alerted. No"
|
||||
" spam."
|
||||
)
|
||||
else:
|
||||
maintainer_alert_needed = True
|
||||
logger.info(f"#{item_number}: Silent edit detected. Alert needed.")
|
||||
|
||||
logger.debug(
|
||||
f"#{item_number} VERDICT: Role={state['last_action_role']}, "
|
||||
f"Idle={days_since_activity:.2f}d"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"last_action_role": state["last_action_role"],
|
||||
"last_action_type": state["last_action_type"],
|
||||
"last_actor_name": state["last_actor_name"],
|
||||
"maintainer_alert_needed": maintainer_alert_needed,
|
||||
"is_stale": is_stale,
|
||||
"days_since_activity": days_since_activity,
|
||||
"days_since_stale_label": days_since_stale_label,
|
||||
"last_comment_text": state["last_comment_text"],
|
||||
"current_labels": labels_list,
|
||||
"stale_threshold_days": STALE_HOURS_THRESHOLD / 24,
|
||||
"close_threshold_days": CLOSE_HOURS_AFTER_STALE_THRESHOLD / 24,
|
||||
"maintainers": maintainers,
|
||||
"issue_author": issue_author,
|
||||
}
|
||||
|
||||
except RequestException as e:
|
||||
return error_response(f"Network Error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unexpected error analyzing #{item_number}: {e}", exc_info=True
|
||||
)
|
||||
return error_response(f"Analysis Error: {e}")
|
||||
|
||||
|
||||
# --- Tool Definitions ---
|
||||
|
||||
|
||||
def _format_days(hours: float) -> str:
|
||||
"""
|
||||
Formats a duration in hours into a clean day string.
|
||||
|
||||
Example:
|
||||
168.0 -> "7"
|
||||
12.0 -> "0.5"
|
||||
"""
|
||||
days = hours / 24
|
||||
return f"{days:.1f}" if days % 1 != 0 else f"{int(days)}"
|
||||
|
||||
|
||||
def add_label_to_issue(item_number: int, label_name: str) -> dict[str, Any]:
|
||||
"""
|
||||
Adds a label to the issue.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
label_name (str): The name of the label to add.
|
||||
"""
|
||||
logger.debug(f"Adding label '{label_name}' to issue #{item_number}.")
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/labels"
|
||||
try:
|
||||
post_request(url, [label_name])
|
||||
return {"status": "success"}
|
||||
except RequestException as e:
|
||||
return error_response(f"Error adding label: {e}")
|
||||
|
||||
|
||||
def remove_label_from_issue(
|
||||
item_number: int, label_name: str
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Removes a label from the issue.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
label_name (str): The name of the label to remove.
|
||||
"""
|
||||
logger.debug(f"Removing label '{label_name}' from issue #{item_number}.")
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/labels/{label_name}"
|
||||
try:
|
||||
delete_request(url)
|
||||
return {"status": "success"}
|
||||
except RequestException as e:
|
||||
return error_response(f"Error removing label: {e}")
|
||||
|
||||
|
||||
def add_stale_label_and_comment(item_number: int) -> dict[str, Any]:
|
||||
"""
|
||||
Marks the issue as stale with a comment and label.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
"""
|
||||
stale_days_str = _format_days(STALE_HOURS_THRESHOLD)
|
||||
close_days_str = _format_days(CLOSE_HOURS_AFTER_STALE_THRESHOLD)
|
||||
|
||||
comment = (
|
||||
"This issue has been automatically marked as stale because it has not"
|
||||
f" had recent activity for {stale_days_str} days after a maintainer"
|
||||
" requested clarification. It will be closed if no further activity"
|
||||
f" occurs within {close_days_str} days."
|
||||
)
|
||||
try:
|
||||
post_request(
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/comments",
|
||||
{"body": comment},
|
||||
)
|
||||
post_request(
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/labels",
|
||||
[STALE_LABEL_NAME],
|
||||
)
|
||||
return {"status": "success"}
|
||||
except RequestException as e:
|
||||
return error_response(f"Error marking issue as stale: {e}")
|
||||
|
||||
|
||||
def alert_maintainer_of_edit(item_number: int) -> dict[str, Any]:
|
||||
"""
|
||||
Posts a comment alerting maintainers of a silent description update.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
"""
|
||||
# Uses the constant signature to ensure detection logic in get_issue_state works.
|
||||
comment = f"{BOT_ALERT_SIGNATURE}. Maintainers, please review."
|
||||
try:
|
||||
post_request(
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/comments",
|
||||
{"body": comment},
|
||||
)
|
||||
return {"status": "success"}
|
||||
except RequestException as e:
|
||||
return error_response(f"Error posting alert: {e}")
|
||||
|
||||
|
||||
def close_as_stale(item_number: int) -> dict[str, Any]:
|
||||
"""
|
||||
Closes the issue as not planned/stale.
|
||||
|
||||
Args:
|
||||
item_number (int): The GitHub issue number.
|
||||
"""
|
||||
days_str = _format_days(CLOSE_HOURS_AFTER_STALE_THRESHOLD)
|
||||
|
||||
comment = (
|
||||
"This has been automatically closed because it has been marked as stale"
|
||||
f" for over {days_str} days."
|
||||
)
|
||||
try:
|
||||
post_request(
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/comments",
|
||||
{"body": comment},
|
||||
)
|
||||
patch_request(
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}",
|
||||
{"state": "closed"},
|
||||
)
|
||||
return {"status": "success"}
|
||||
except RequestException as e:
|
||||
return error_response(f"Error closing issue: {e}")
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=LLM_MODEL_NAME,
|
||||
name="adk_repository_auditor_agent",
|
||||
description="Audits open issues.",
|
||||
instruction=PROMPT_TEMPLATE.format(
|
||||
OWNER=OWNER,
|
||||
REPO=REPO,
|
||||
STALE_LABEL_NAME=STALE_LABEL_NAME,
|
||||
REQUEST_CLARIFICATION_LABEL=REQUEST_CLARIFICATION_LABEL,
|
||||
stale_threshold_days=STALE_HOURS_THRESHOLD / 24,
|
||||
close_threshold_days=CLOSE_HOURS_AFTER_STALE_THRESHOLD / 24,
|
||||
),
|
||||
tools=[
|
||||
add_label_to_issue,
|
||||
add_stale_label_and_comment,
|
||||
alert_maintainer_of_edit,
|
||||
close_as_stale,
|
||||
get_issue_state,
|
||||
remove_label_from_issue,
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user