Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/issue-monitor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.

name: ADK Issue Monitoring Agent

on:
schedule:
# Runs daily at 6:00 AM UTC
- cron: '0 6 * * *'

# Allows manual triggering from the GitHub Actions tab
workflow_dispatch:
inputs:
full_scan:
description: 'Run an Initial Full Scan of ALL open issues'
required: false
type: boolean
default: false

jobs:
sweep-spam:
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
issues: write
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests google-adk python-dotenv

- name: Run Issue Monitoring Agent
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
CONCURRENCY_LIMIT: 3
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
run: python -m spam_sweeper_agent.main
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
You are the automated security and moderation agent for the {OWNER}/{REPO} repository.

You will be provided with an Issue Number and a list of comments made by non-maintainers.
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.

CRITERIA FOR SPAM:
- The comment is completely unrelated to the repository or the specific issue.
- The comment promotes a 3rd party product, service, or website.
- The comment is generic "SEO spam" (e.g., "Great post! Check out my site at [link]").

INSTRUCTIONS:
1. Evaluate the provided comments.
2. If you identify spam, call the `flag_issue_as_spam` tool.
- Pass the `item_number`.
- Pass a brief `detection_reason` explaining which comment is spam and why (e.g., "@spammer_bot posted an irrelevant link to a shoe store").
3. If NONE of the comments contain spam, do NOT call any tools. Just respond with "No spam detected."

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.
62 changes: 62 additions & 0 deletions contributing/samples/adk_issue_monitoring_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADK Issue Monitoring Agent 🛡️

An intelligent, cost-optimized, automated moderation agent built with the **Google Agent Development Kit (ADK)**.

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.

## ✨ Key Features & Optimizations

* **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.
* **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).
* **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.
* **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.

---

## Configuration

The agent is configured via environment variables, typically set as secrets in GitHub Actions.

### Required Secrets

| Secret Name | Description |
| :--- | :--- |
| `GITHUB_TOKEN` | A GitHub Personal Access Token (PAT) or Service Account Token with `repo` and `issues: write` scope. |
| `GOOGLE_API_KEY` | An API key for the Google AI (Gemini) model used for reasoning. |

### Optional Configuration

These variables control the scanning behavior, thresholds, and model selection.

| Variable Name | Description | Default |
| :--- | :--- | :--- |
| `INITIAL_FULL_SCAN` | If `true`, audits every open issue in the repository. If `false`, only audits issues updated in the last 24 hours. | `false` |
| `SPAM_LABEL_NAME` | The exact text of the label applied to flagged issues. | `spam` |
| `BOT_NAME` | The GitHub username of your official bot to ensure its comments are ignored. | `adk-bot` |
| `CONCURRENCY_LIMIT` | The number of issues to process concurrently. | `3` |
| `SLEEP_BETWEEN_CHUNKS` | Time in seconds to sleep between batches to respect GitHub API rate limits. | `1.5` |
| `LLM_MODEL_NAME`| The specific Gemini model version to use. | `gemini-2.5-flash` |
| `OWNER` | Repository owner (auto-detected in Actions). | (Environment dependent) |
| `REPO` | Repository name (auto-detected in Actions). | (Environment dependent) |

---

## Deployment

To deploy this agent, a GitHub Actions workflow file (`.github/workflows/issue-monitor.yml`) is recommended.

### Directory Structure Note
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.

### Example Workflow Execution
```yaml
- name: Run ADK Issue Monitoring Agent
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
# Mapped to the manual trigger checkbox in the GitHub UI
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
PYTHONPATH: contributing/samples
run: python -m adk_issue_monitoring_agent.main
15 changes: 15 additions & 0 deletions contributing/samples/adk_issue_monitoring_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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 . import agent
88 changes: 88 additions & 0 deletions contributing/samples/adk_issue_monitoring_agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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.

import logging
import os
from typing import Any

from adk_issue_monitoring_agent.settings import BOT_ALERT_SIGNATURE
from adk_issue_monitoring_agent.settings import GITHUB_BASE_URL
from adk_issue_monitoring_agent.settings import LLM_MODEL_NAME
from adk_issue_monitoring_agent.settings import OWNER
from adk_issue_monitoring_agent.settings import REPO
from adk_issue_monitoring_agent.settings import SPAM_LABEL_NAME
from adk_issue_monitoring_agent.utils import error_response
from adk_issue_monitoring_agent.utils import post_request
from google.adk.agents.llm_agent import Agent
from requests.exceptions import RequestException

logger = logging.getLogger("google_adk." + __name__)


def load_prompt_template(filename: str) -> str:
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")

# --- Tools ---


def flag_issue_as_spam(
item_number: int, detection_reason: str
) -> dict[str, Any]:
"""
Flags an issue as spam by adding a label and leaving a comment for maintainers.

Args:
item_number (int): The GitHub issue number.
detection_reason (str): The explanation of what the spam is.
"""
logger.info(f"Flagging #{item_number} as SPAM. Reason: {detection_reason}")

label_url = (
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/labels"
)
comment_url = (
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{item_number}/comments"
)

alert_body = (
f"{BOT_ALERT_SIGNATURE}\n"
"@maintainers, a suspected spam comment was detected in this thread.\n\n"
f"**Reason:** {detection_reason}"
)

try:
# 1. Add Label
post_request(label_url, {"labels": [SPAM_LABEL_NAME]})
# 2. Post Alert Comment
post_request(comment_url, {"body": alert_body})
return {"status": "success", "message": "Maintainers alerted successfully."}
except RequestException as e:
return error_response(f"Error flagging issue: {e}")


root_agent = Agent(
model=LLM_MODEL_NAME,
name="spam_auditor_agent",
description="Audits issue comments for spam.",
instruction=PROMPT_TEMPLATE.format(
OWNER=OWNER,
REPO=REPO,
),
tools=[flag_issue_as_spam],
)
Loading
Loading