Skip to content

Commit ab79e51

Browse files
Merge branch 'main' into fix/enforce-arg-types-4612
2 parents 009e41a + 6f0dcb3 commit ab79e51

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+6204
-4500
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: ADK Issue Monitoring Agent
16+
17+
on:
18+
schedule:
19+
# Runs daily at 6:00 AM UTC
20+
- cron: '0 6 * * *'
21+
22+
# Allows manual triggering from the GitHub Actions tab
23+
workflow_dispatch:
24+
inputs:
25+
full_scan:
26+
description: 'Run an Initial Full Scan of ALL open issues'
27+
required: false
28+
type: boolean
29+
default: false
30+
31+
jobs:
32+
sweep-spam:
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 120
35+
permissions:
36+
issues: write
37+
contents: read
38+
39+
steps:
40+
- name: Checkout repository
41+
uses: actions/checkout@v6
42+
43+
- name: Set up Python
44+
uses: actions/setup-python@v6
45+
with:
46+
python-version: '3.11'
47+
48+
- name: Install dependencies
49+
run: |
50+
python -m pip install --upgrade pip
51+
pip install requests google-adk python-dotenv
52+
53+
- name: Run Issue Monitoring Agent
54+
env:
55+
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
56+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
57+
OWNER: ${{ github.repository_owner }}
58+
REPO: ${{ github.event.repository.name }}
59+
CONCURRENCY_LIMIT: 3
60+
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
61+
LLM_MODEL_NAME: "gemini-2.5-flash"
62+
PYTHONPATH: contributing/samples
63+
run: python -m adk_issue_monitoring_agent.main

.github/workflows/release-cherry-pick.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
cherry-pick:
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v6
2222
with:
2323
ref: release/candidate
2424
fetch-depth: 0

.github/workflows/release-cut.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
cut-release:
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v6
2222
with:
2323
ref: ${{ inputs.commit_sha || 'main' }}
2424

.github/workflows/release-finalize.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
echo "is_release_pr=false" >> $GITHUB_OUTPUT
3030
fi
3131
32-
- uses: actions/checkout@v4
32+
- uses: actions/checkout@v6
3333
if: steps.check.outputs.is_release_pr == 'true'
3434
with:
3535
ref: release/candidate

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
echo "exists=false" >> $GITHUB_OUTPUT
2828
fi
2929
30-
- uses: actions/checkout@v4
30+
- uses: actions/checkout@v6
3131
if: steps.check.outputs.exists == 'true'
3232
with:
3333
ref: release/candidate

.github/workflows/release-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
echo "version=$VERSION" >> $GITHUB_OUTPUT
2929
echo "Publishing version: $VERSION"
3030
31-
- uses: actions/checkout@v4
31+
- uses: actions/checkout@v6
3232

3333
- name: Install uv
3434
uses: astral-sh/setup-uv@v4
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
You are the automated security and moderation agent for the {OWNER}/{REPO} repository.
2+
3+
You will be provided with an Issue Number and a list of comments made by non-maintainers.
4+
Your job is to read through these comments and identify if any of them contain SPAM, promotional content for 3rd-party websites, SEO links, or objectionable material.
5+
6+
CRITERIA FOR SPAM:
7+
- The comment is completely unrelated to the repository or the specific issue.
8+
- The comment promotes a 3rd party product, service, or website.
9+
- The comment is generic "SEO spam" (e.g., "Great post! Check out my site at [link]").
10+
11+
INSTRUCTIONS:
12+
1. Evaluate the provided comments.
13+
2. If you identify spam, call the `flag_issue_as_spam` tool.
14+
- Pass the `item_number`.
15+
- Pass a brief `detection_reason` explaining which comment is spam and why (e.g., "@spammer_bot posted an irrelevant link to a shoe store").
16+
3. If NONE of the comments contain spam, do NOT call any tools. Just respond with "No spam detected."
17+
18+
Remember: Do not flag comments that are merely unhelpful, off-topic, or from beginners asking legitimate questions. Only flag actual spam, endorsements, or objectionable material.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ADK Issue Monitoring Agent 🛡️
2+
3+
An intelligent, cost-optimized, automated moderation agent built with the **Google Agent Development Kit (ADK)**.
4+
5+
This agent automatically audits GitHub repository issues to detect SEO spam, unsolicited promotional links, and irrelevant third-party endorsements. If spam is detected, it automatically applies a `spam` label and alerts the repository maintainers.
6+
7+
## ✨ Key Features & Optimizations
8+
9+
* **Zero-Waste LLM Invocations:** Fetches issue comments via REST APIs and pre-filters them in Python. It automatically ignores comments from maintainers, `[bot]` accounts, and the official `adk-bot`. The Gemini LLM is never invoked for safe threads, saving 100% of the token cost.
10+
* **Dual-Mode Scanning:** Can perform a **Deep Clean** (auditing the entire history of all open issues) or a **Daily Sweep** (only fetching issues updated within the last 24 hours).
11+
* **Token Truncation:** Uses Regular Expressions to strip out Markdown code blocks (` ``` `) replacing them with `[CODE BLOCK REMOVED]`, and truncates unusually long text to 1,500 characters before sending it to the AI.
12+
* **Idempotency (Anti-Double-Posting):** The bot reads the comment history for its own signature. If it has already flagged an issue, it instantly skips it, preventing infinite feedback loops.
13+
14+
---
15+
16+
## Configuration
17+
18+
The agent is configured via environment variables, typically set as secrets in GitHub Actions.
19+
20+
### Required Secrets
21+
22+
| Secret Name | Description |
23+
| :--- | :--- |
24+
| `GITHUB_TOKEN` | A GitHub Personal Access Token (PAT) or Service Account Token with `repo` and `issues: write` scope. |
25+
| `GOOGLE_API_KEY` | An API key for the Google AI (Gemini) model used for reasoning. |
26+
27+
### Optional Configuration
28+
29+
These variables control the scanning behavior, thresholds, and model selection.
30+
31+
| Variable Name | Description | Default |
32+
| :--- | :--- | :--- |
33+
| `INITIAL_FULL_SCAN` | If `true`, audits every open issue in the repository. If `false`, only audits issues updated in the last 24 hours. | `false` |
34+
| `SPAM_LABEL_NAME` | The exact text of the label applied to flagged issues. | `spam` |
35+
| `BOT_NAME` | The GitHub username of your official bot to ensure its comments are ignored. | `adk-bot` |
36+
| `CONCURRENCY_LIMIT` | The number of issues to process concurrently. | `3` |
37+
| `SLEEP_BETWEEN_CHUNKS` | Time in seconds to sleep between batches to respect GitHub API rate limits. | `1.5` |
38+
| `LLM_MODEL_NAME`| The specific Gemini model version to use. | `gemini-2.5-flash` |
39+
| `OWNER` | Repository owner (auto-detected in Actions). | (Environment dependent) |
40+
| `REPO` | Repository name (auto-detected in Actions). | (Environment dependent) |
41+
42+
---
43+
44+
## Deployment
45+
46+
To deploy this agent, a GitHub Actions workflow file (`.github/workflows/issue-monitor.yml`) is recommended.
47+
48+
### Directory Structure Note
49+
Because this agent resides within the `adk-python` package structure, the workflow must ensure the script is executed correctly to handle imports. It must be run as a module from the parent directory.
50+
51+
### Example Workflow Execution
52+
```yaml
53+
- name: Run ADK Issue Monitoring Agent
54+
env:
55+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
57+
OWNER: ${{ github.repository_owner }}
58+
REPO: ${{ github.event.repository.name }}
59+
# Mapped to the manual trigger checkbox in the GitHub UI
60+
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
61+
PYTHONPATH: contributing/samples
62+
run: python -m adk_issue_monitoring_agent.main
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import os
17+
from typing import Any
18+
19+
from adk_issue_monitoring_agent.settings import BOT_ALERT_SIGNATURE
20+
from adk_issue_monitoring_agent.settings import GITHUB_BASE_URL
21+
from adk_issue_monitoring_agent.settings import LLM_MODEL_NAME
22+
from adk_issue_monitoring_agent.settings import OWNER
23+
from adk_issue_monitoring_agent.settings import REPO
24+
from adk_issue_monitoring_agent.settings import SPAM_LABEL_NAME
25+
from adk_issue_monitoring_agent.utils import error_response
26+
from adk_issue_monitoring_agent.utils import get_issue_comments
27+
from adk_issue_monitoring_agent.utils import get_issue_details
28+
from adk_issue_monitoring_agent.utils import post_request
29+
from google.adk.agents.llm_agent import Agent
30+
from requests.exceptions import RequestException
31+
32+
logger = logging.getLogger("google_adk." + __name__)
33+
34+
35+
def load_prompt_template(filename: str) -> str:
36+
file_path = os.path.join(os.path.dirname(__file__), filename)
37+
with open(file_path, "r") as f:
38+
return f.read()
39+
40+
41+
PROMPT_TEMPLATE = load_prompt_template("PROMPT_INSTRUCTION.txt")
42+
43+
# --- Tools ---
44+
45+
46+
def flag_issue_as_spam(
47+
item_number: int, detection_reason: str
48+
) -> dict[str, Any]:
49+
"""
50+
Flags an issue as spam by adding a label and leaving a comment for maintainers.
51+
Includes idempotency checks to avoid duplicate POST actions.
52+
53+
Args:
54+
item_number (int): The GitHub issue number.
55+
detection_reason (str): The explanation of what the spam is.
56+
"""
57+
logger.info(f"Flagging #{item_number} as SPAM. Reason: {detection_reason}")
58+
59+
label_url = (
60+
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/labels"
61+
)
62+
comment_url = (
63+
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/comments"
64+
)
65+
66+
safe_reason = detection_reason.replace("```", "'''")
67+
68+
alert_body = (
69+
f"{BOT_ALERT_SIGNATURE}\n"
70+
"@maintainers, a suspected spam comment was detected in this thread.\n\n"
71+
"**Reason:**\n"
72+
f"```text\n{safe_reason}\n```"
73+
)
74+
75+
try:
76+
# 1. Fetch current state to check what actions are actually needed
77+
issue = get_issue_details(OWNER, REPO, item_number)
78+
comments = get_issue_comments(OWNER, REPO, item_number)
79+
80+
current_labels = [
81+
label["name"].lower() for label in issue.get("labels", [])
82+
]
83+
is_labeled = SPAM_LABEL_NAME.lower() in current_labels
84+
is_commented = any(
85+
BOT_ALERT_SIGNATURE in c.get("body", "") for c in comments
86+
)
87+
88+
if is_labeled and is_commented:
89+
logger.info(f"#{item_number} is already labeled and commented. Skipping.")
90+
elif is_labeled and not is_commented:
91+
post_request(comment_url, {"body": alert_body})
92+
logger.info(f"Successfully posted spam alert comment to #{item_number}.")
93+
elif not is_labeled and is_commented:
94+
post_request(label_url, {"labels": [SPAM_LABEL_NAME]})
95+
logger.info(
96+
f"Successfully added '{SPAM_LABEL_NAME}' label to #{item_number}."
97+
)
98+
else:
99+
post_request(label_url, {"labels": [SPAM_LABEL_NAME]})
100+
post_request(comment_url, {"body": alert_body})
101+
logger.info(f"Successfully fully flagged #{item_number}.")
102+
103+
return {"status": "success", "message": "Maintainers alerted successfully."}
104+
105+
except RequestException as e:
106+
return error_response(f"Error flagging issue: {e}")
107+
108+
109+
root_agent = Agent(
110+
model=LLM_MODEL_NAME,
111+
name="spam_auditor_agent",
112+
description="Audits issue comments for spam.",
113+
instruction=PROMPT_TEMPLATE.format(
114+
OWNER=OWNER,
115+
REPO=REPO,
116+
),
117+
tools=[flag_issue_as_spam],
118+
)

0 commit comments

Comments
 (0)