From a4d1482b09f347d98e9d3330a1b70110aa67ba02 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 15 Apr 2026 14:31:13 +0900 Subject: [PATCH 1/7] feat: add agentic issue tagging, PR description quality and security review --- .github/workflows/pr-quality-check.yml | 33 ++++ .github/workflows/security-review.yml | 42 ++++ .github/workflows/triage.yml | 31 +++ scripts/pr_checker_agent.py | 161 ++++++++++++++++ scripts/security_review_agent.py | 214 +++++++++++++++++++++ scripts/triage_agent.py | 256 +++++++++++++++++++++++++ 6 files changed, 737 insertions(+) create mode 100644 .github/workflows/pr-quality-check.yml create mode 100644 .github/workflows/security-review.yml create mode 100644 .github/workflows/triage.yml create mode 100644 scripts/pr_checker_agent.py create mode 100644 scripts/security_review_agent.py create mode 100644 scripts/triage_agent.py diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml new file mode 100644 index 000000000..9790283c4 --- /dev/null +++ b/.github/workflows/pr-quality-check.yml @@ -0,0 +1,33 @@ +name: PR Quality Check +on: + pull_request: + types: [opened, reopened] + +jobs: + pr_quality_check: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run PR quality check agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} + AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO_NAME: ${{ github.repository }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: python scripts/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml new file mode 100644 index 000000000..08751138b --- /dev/null +++ b/.github/workflows/security-review.yml @@ -0,0 +1,42 @@ +name: Security Review +on: + pull_request: + types: [opened, reopened] + issue_comment: + types: [created] + +jobs: + security-review: + runs-on: ubuntu-latest + # Always runs on PR creation + # Also runs if comment on PR contains "/security-review" + if: > + github.event_name == 'pull_request' || + ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + contains(github.event.comment.body, '/security-review') + ) + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run security review agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + REPO_NAME: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + TRIGGER: ${{ github.event_name }} + run: python scripts/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000..b52bb72a0 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,31 @@ +name: Issue Triage +on: + issues: + types: [opened, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install litellm PyGithub + - name: Run triage agent + env: + # e.g: "claude-sonnet-4-6", "gpt-4o", etc. + MODEL: ${{ secrets.MODEL }} + # Only API key for the chosen model is required + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Obtained automatically by GH Actions + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO_NAME: ${{ github.repository }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + run: python scripts/triage_agent.py diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py new file mode 100644 index 000000000..1b115a5fe --- /dev/null +++ b/scripts/pr_checker_agent.py @@ -0,0 +1,161 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) +author = os.environ["AUTHOR_USERNAME"] + +MODEL = os.environ["MODEL"] +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "post_comment", + "description": ( + "Post a comment on the PR. Use this to welcome a first-time contributor, " + "ask for a clearer description, request an issue link, or flag non-compliance " + "with CONTRIBUTING.md. Combine multiple concerns into a single comment where " + "possible rather than posting several separate ones." + ), + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. +Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, +you must check the following - in this order: + +1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. + Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. + +2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what + the change does and why, ask for a clearer description. + +3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like + "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, + ask the author to either link an existing issue or create a new one. + +4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or + requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section + and point out specifically what needs to change. + +Important rules: +- If multiple concerns apply, combine them into a single comment, never post more than one. +- If everything looks good, stay silent. Do not post a comment just to say things look fine. +- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. +- When referencing CONTRIBUTING.md requirements, be specific: quote or paraphrase the rule, + don't just say "please read the contributing guide". +- Most importantly, be as succinct as possible.""" + +# GitHub helpers + +def get_contributing_md() -> str: + """Fetches CONTRIBUTING.md from the repo root, or returns a notice if absent.""" + try: + contents = repo.get_contents("CONTRIBUTING.md") + return contents.decoded_content.decode("utf-8") + except Exception: + return "(No CONTRIBUTING.md found in this repository.)" + + +def is_first_contribution() -> bool: + """Returns True if the author has no previously merged PRs in this repo.""" + first_contribution_list = ['FIRST_TIMER', 'FIRST_TIME_CONTRIBUTOR', 'NONE'] + return os.environ["AUTHOR_ASSOCIATION"] in first_contribution_list + + +def post_comment(body: str) -> str: + pr.create_issue_comment(body) + return "Comment posted." + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_comment": + result = post_comment(inputs["body"]) + else: + result = f"Unknown tool: {name}" + + print(f"[tool] {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + first_contribution = is_first_contribution() + contributing_md = get_contributing_md() + + return ( + f"Please review this newly opened PR:\n\n" + f"Title: {os.environ['PR_TITLE']}\n" + f"Author: {author} ({'first-time contributor' if first_contribution else 'returning contributor'})\n" + f"Description:\n{os.environ.get('PR_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"CONTRIBUTING.md contents:\n\n" + f"{contributing_md}" + ) + + +def run_pr_review_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + + if message.content: + print(f"[agent] {message.content}") + + messages.append(message.model_dump(exclude_none=True)) + + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_pr_review_agent() diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py new file mode 100644 index 000000000..6e1184d6c --- /dev/null +++ b/scripts/security_review_agent.py @@ -0,0 +1,214 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +pr = repo.get_pull(int(os.environ["PR_NUMBER"])) + +MODEL = os.environ["MODEL"] +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + + +# Exclude files that are not useful for security analysis +IGNORED_FILENAMES = { + "package-lock.json", + "yarn.lock", + "poetry.lock", + "Gemfile.lock", + "Cargo.lock", + "composer.lock", + "pnpm-lock.yaml", + "pip.lock", +} + +IGNORED_EXTENSIONS = {".lock", ".sum"} + +# Truncate very large diffs like generated files to prevent bloating the prompt +MAX_PATCH_CHARS_PER_FILE = 3000 + +# System prompt + +SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. +You are given the diff of a pull request and must identify potential security issues. + +Focus only on security-relevant concerns such as: +- Hardcoded secrets, tokens, passwords or API keys +- Injection vulnerabilities (SQL, shell, template, etc.) +- Insecure use of cryptography or hashing +- Unsafe deserialization +- Path traversal or directory traversal risks +- Insecure direct object references +- Missing input validation or sanitisation on user-controlled data +- Use of known-vulnerable dependency versions (if visible in the diff) +- Overly permissive file or network access + +Do NOT comment on code style, performance, test coverage, or general best practices +unless they have a direct security implication. + +If you find no issues, say so clearly and briefly — do not invent concerns. +Format your response as a markdown comment suitable for posting directly on a GitHub PR. +Start with a short summary line, then list findings with file references where applicable. +If there are no findings, keep the response to 2-3 sentences maximum.""" + +# GitHub helpers + +def get_pr_diff() -> str: + """ + Fetches changed files and their patches, filtering out lockfiles and + other noise. Returns a formatted string ready to be included in the prompt. + """ + sections = [] + for f in pr.get_files(): + filename = os.path.basename(f.filename) + _, ext = os.path.splitext(filename) + + if filename in IGNORED_FILENAMES or ext in IGNORED_EXTENSIONS: + print(f"[diff] Skipping {f.filename} (ignored file type)") + continue + + if not f.patch: + print(f"[diff] Skipping {f.filename} (no patch — binary or too large)") + continue + + patch = f.patch[:MAX_PATCH_CHARS_PER_FILE] + truncated = len(f.patch) > MAX_PATCH_CHARS_PER_FILE + sections.append( + f"### {f.filename}\n```diff\n{patch}" + + ("\n... (truncated)" if truncated else "") + + "\n```" + ) + + return "\n\n".join(sections) if sections else "(no reviewable changes found)" + + +def find_previous_security_comment() -> object | None: + """ + Looks for an existing security review comment posted by github-actions[bot] + so we can replace it rather than stacking multiple comments on updated reviews. + """ + for comment in pr.get_issue_comments(): + if ( + comment.user.login == "github-actions[bot]" + and "🔒 Automated Security Review" in comment.body + ): + return comment + return None + + +def post_or_update_comment(body: str): + """ + If a previous security review comment exists, edit it in place. + Otherwise post a new one to keep the PR timeline clean. + """ + existing = find_previous_security_comment() + if existing: + existing.edit(body) + print("[comment] Updated existing security review comment.") + else: + pr.create_issue_comment(body) + print("[comment] Posted new security review comment.") + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "post_security_review", + "description": ( + "Post the security review findings as a comment on the PR. " + "Call this once when your analysis is complete. " + "If there are no findings, still call this to confirm the review ran." + ), + "parameters": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The full markdown comment body to post on the PR.", + } + }, + "required": ["body"], + }, + }, + } +] + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "post_security_review": + # Prepend a header to identify review comments across runs + body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" + post_or_update_comment(body) + return "Security review comment posted." + return f"Unknown tool: {name}" + +# Agentic loop + +def build_initial_message() -> str: + trigger = os.environ.get("TRIGGER", "pull_request") + trigger_note = ( + "This review was requested manually via `/security-review`." + if trigger == "issue_comment" + else "This review was triggered automatically on PR creation." + ) + + return ( + f"Please perform a security review of this pull request.\n\n" + f"**PR #{pr.number}:** {pr.title}\n" + f"_{trigger_note}_\n\n" + f"---\n\n" + f"{get_pr_diff()}" + ) + + +def run_security_review_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + + if message.content: + print(f"[agent] {message.content}") + + messages.append(message.model_dump(exclude_none=True)) + + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + print(f"[tool] {tool_call.function.name}: {result}") + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_security_review_agent() diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py new file mode 100644 index 000000000..f5fdbb18d --- /dev/null +++ b/scripts/triage_agent.py @@ -0,0 +1,256 @@ +import os +import json +import litellm +from github import Github, Auth + +# Setup + +gh = Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) +repo = gh.get_repo(os.environ["REPO_NAME"]) +issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) + +LATEST_ISSUES_LIMIT = 100 +MODEL = os.environ["MODEL"] + +for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: + if not os.environ[env_var]: + raise ValueError(f"{env_var} is not set") + +valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] +if not any(os.environ.get(api_key) for api_key in valid_api_keys): + raise ValueError("No API key is set") + +# Tools + +TOOLS = [ + { + "type": "function", + "function": { + "name": "apply_label", + "description": ( + "Apply one or more labels to the issue. " + "Use labels like: automation, bug, dependencies, " + "documentation, enhancement, good-first-issue, " + "meeting, needs-info, plugins, protocol, question, " + "security, tech-debt, testing." + ), + "parameters": { + "type": "object", + "properties": { + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "List of labels to apply.", + } + }, + "required": ["labels"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "post_comment", + "description": "Post a comment on the issue, e.g. to ask for clarification or acknowledge receipt.", + "parameters": { + "type": "object", + "properties": { + "body": {"type": "string", "description": "The comment text (markdown supported)."} + }, + "required": ["body"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "mark_duplicate", + "description": ( + "Mark this issue as a duplicate of an existing one. " + "Use this when the issue is clearly asking about the same thing as an open issue. " + "Post a comment pointing to the original issue without closing anything." + ), + "parameters": { + "type": "object", + "properties": { + "original_issue_number": { + "type": "integer", + "description": "The issue number this is a duplicate of.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues are duplicates.", + }, + }, + "required": ["original_issue_number", "reason"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "suggest_possible_duplicate", + "description": ( + "Use when an existing issue is related but not clearly the same thing. " + "Posts a comment pointing to the similar issue without closing anything." + "Continue triage normally after posting the comment." + ), + "parameters": { + "type": "object", + "properties": { + "related_issue_number": { + "type": "integer", + "description": "The issue number that might be related.", + }, + "reason": { + "type": "string", + "description": "Brief explanation of why these issues seem related.", + }, + }, + "required": ["related_issue_number", "reason"], + }, + }, + }, +] + +# System prompt + +SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. +Given a new issue and a list of existing open issues, you must: + +1. Check whether the new issue is a duplicate of an existing one. + - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. + - If it seems related but could be distinct, call suggest_possible_duplicate. That comment + will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. +2. Otherwise, classify it by applying appropriate labels + (bug, feature-request, question, documentation, needs-info, good-first-issue). +3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), + post a friendly comment asking for it. +4. If no possible duplicate was flagged, post a short acknowledgment comment so the + author knows their issue was received. Do NOT post comments on administrative issues + such as meeting minutes, roadmaps, etc. + +Keep comments concise and friendly.""" + +# GitHub helpers + +def get_existing_issues(limit: int = LATEST_ISSUES_LIMIT) -> str: + """ + Fetches the most recent open issues (excluding the current one) + and formats them into a string for the prompt. + """ + open_issues = repo.get_issues(state="open") + lines = [] + count = 0 + for existing in open_issues: + if existing.number == issue.number: + continue + lines.append( + f"- #{existing.number}: {existing.title}\n" + f" {(existing.body or '').strip()[:200]}" # truncate long bodies + ) + count += 1 + if count >= limit: + break + return "\n".join(lines) if lines else "(no other open issues)" + + +def apply_label(labels: list[str]) -> str: + existing_label_names = [l.name for l in repo.get_labels()] + for label in labels: + if label not in existing_label_names: + repo.create_label(label, "ededed") + issue.add_to_labels(*labels) + return f"Applied labels: {labels}" + + +def post_comment(body: str) -> str: + issue.create_comment(body) + return "Comment posted." + + +def mark_duplicate(original_issue_number: int, reason: str) -> str: + original = repo.get_issue(original_issue_number) + issue.create_comment( + f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " + f"({original.html_url}).\n\n> {reason}\n\n" + f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." + ) + issue.add_to_labels("duplicate") + return f"Marked as duplicate of #{original_issue_number}." + + +def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: + related = repo.get_issue(related_issue_number) + issue.create_comment( + f"Hey! This might be related to #{related_issue_number} " + f"({related.html_url}) — {reason}\n\n" + f"Feel free to check if that one already covers what you're reporting!" + ) + return f"Flagged as possibly related to #{related_issue_number}." + + +# Tool dispatch + +def handle_tool_call(name: str, inputs: dict) -> str: + if name == "apply_label": + result = apply_label(inputs["labels"]) + elif name == "post_comment": + result = post_comment(inputs["body"]) + elif name == "mark_duplicate": + result = mark_duplicate(inputs["original_issue_number"], inputs["reason"]) + elif name == "suggest_possible_duplicate": + result = suggest_possible_duplicate(inputs["related_issue_number"], inputs["reason"]) + else: + result = f"Unknown tool: {name}" + print(f"Tool {name}: {result}") + return result + +# Agentic loop + +def build_initial_message() -> str: + return ( + f"Please triage this new GitHub issue:\n\n" + f"Title: {os.environ['ISSUE_TITLE']}\n" + f"Body:\n{os.environ.get('ISSUE_BODY') or '(no description provided)'}\n\n" + f"---\n" + f"Here are the currently open issues for duplicate detection:\n\n" + f"{get_existing_issues()}" + ) + + +def run_triage_agent(): + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_initial_message()}, + ] + + while True: + response = litellm.completion( + model=MODEL, + messages=messages, + tools=TOOLS, + ) + + message = response.choices[0].message + messages.append(message.model_dump(exclude_none=True)) + + finish_reason = response.choices[0].finish_reason + if finish_reason == "stop" or not message.tool_calls: + break + + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + + messages.extend(tool_results) + + +if __name__ == "__main__": + run_triage_agent() From cdd01cf314ec0694b418f6063ea7374bc3014c9b Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 15 Apr 2026 15:57:23 +0900 Subject: [PATCH 2/7] chore: update PR workflows to run on pull_request_target --- .github/workflows/pr-quality-check.yml | 2 +- .github/workflows/security-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 9790283c4..8ccaaa6ea 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -1,6 +1,6 @@ name: PR Quality Check on: - pull_request: + pull_request_target: types: [opened, reopened] jobs: diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 08751138b..38c5194c4 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -1,6 +1,6 @@ name: Security Review on: - pull_request: + pull_request_target: types: [opened, reopened] issue_comment: types: [created] From ea6509abeb4c205a11b9ec0aa0875d9add342d10 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 13:05:00 +0900 Subject: [PATCH 3/7] refactor: optimize system prompts and standardize outputs --- scripts/pr_checker_agent.py | 39 ++++++++++++------------- scripts/security_review_agent.py | 49 ++++++++++++++++++-------------- scripts/triage_agent.py | 43 +++++++++++++++------------- 3 files changed, 70 insertions(+), 61 deletions(-) diff --git a/scripts/pr_checker_agent.py b/scripts/pr_checker_agent.py index 1b115a5fe..74c0cc12d 100644 --- a/scripts/pr_checker_agent.py +++ b/scripts/pr_checker_agent.py @@ -47,30 +47,30 @@ # System prompt SYSTEM_PROMPT = """You are a PR review assistant for an open-source GitHub repository. -Given a newly opened PR, its author's contribution history, and the repository's CONTRIBUTING.md, -you must check the following - in this order: +Check the following in order, then post at most one comment combining all concerns. If nothing needs flagging, stay silent. -1. FIRST CONTRIBUTION: If this is the author's first contribution to the repo, welcome them warmly. - Acknowledge their effort and point them to any relevant getting-started resources in CONTRIBUTING.md. +Checks: +1. FIRST CONTRIBUTION: Welcome first-time contributors and link any getting-started resources from CONTRIBUTING.md. +2. DESCRIPTION: If missing or too vague to explain what changed and why, ask for clarification. +3. LINKED ISSUE: If no "Fixes/Closes/Resolves/Related to #N" link exists, ask the author to add one. +4. CONTRIBUTING.md: If the PR doesn't follow the required structure, quote the specific rule that is violated. -2. DESCRIPTION CLARITY: If the PR description is missing, too vague, or doesn't explain what - the change does and why, ask for a clearer description. +Rules: +- One comment maximum. Combine all concerns. +- Silence if everything is fine. +- Be constructive, not demanding. +- No emojis. -3. LINKED ISSUE: Check whether the description contains a linked issue using keywords like - "Fixes #N", "Closes #N", "Resolves #N", or "Related to #N". If no issue is linked, - ask the author to either link an existing issue or create a new one. +When posting a comment, always use this exact structure (omit sections that don't apply): -4. CONTRIBUTING.md COMPLIANCE: Check whether the PR description follows the structure or - requirements defined in CONTRIBUTING.md. If it doesn't comply, quote the relevant section - and point out specifically what needs to change. + (first-time contributors only) -Important rules: -- If multiple concerns apply, combine them into a single comment, never post more than one. -- If everything looks good, stay silent. Do not post a comment just to say things look fine. -- Be warm and constructive, never demanding. Remember this may be someone's first open-source contribution. -- When referencing CONTRIBUTING.md requirements, be specific: quote or paraphrase the rule, - don't just say "please read the contributing guide". -- Most importantly, be as succinct as possible.""" + + + + + +... (repeat for each rule that is violated)""" # GitHub helpers @@ -132,6 +132,7 @@ def run_pr_review_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py index 6e1184d6c..51ff803bf 100644 --- a/scripts/security_review_agent.py +++ b/scripts/security_review_agent.py @@ -39,26 +39,30 @@ # System prompt SYSTEM_PROMPT = """You are a security analysis assistant for a GitHub repository. -You are given the diff of a pull request and must identify potential security issues. - -Focus only on security-relevant concerns such as: -- Hardcoded secrets, tokens, passwords or API keys -- Injection vulnerabilities (SQL, shell, template, etc.) -- Insecure use of cryptography or hashing -- Unsafe deserialization -- Path traversal or directory traversal risks -- Insecure direct object references -- Missing input validation or sanitisation on user-controlled data -- Use of known-vulnerable dependency versions (if visible in the diff) -- Overly permissive file or network access - -Do NOT comment on code style, performance, test coverage, or general best practices -unless they have a direct security implication. - -If you find no issues, say so clearly and briefly — do not invent concerns. -Format your response as a markdown comment suitable for posting directly on a GitHub PR. -Start with a short summary line, then list findings with file references where applicable. -If there are no findings, keep the response to 2-3 sentences maximum.""" +You are given a pull request diff and must identify potential security issues. + +Flag only: hardcoded secrets or credentials, injection vulnerabilities (SQL, shell, template), insecure cryptography or hashing, unsafe deserialization, path traversal, missing input validation on user-controlled data, known-vulnerable dependency versions, overly permissive file or network access. + +Do not comment on style, performance, test coverage, or best practices unless directly tied to a security risk. + +Always call post_security_review once when done, even if there are no findings. +No emojis. + +Use this exact format: + +### Summary + + +### Findings (omit section if none) + +**** + + + + + +... (repeat for each finding) +""" # GitHub helpers @@ -99,7 +103,7 @@ def find_previous_security_comment() -> object | None: for comment in pr.get_issue_comments(): if ( comment.user.login == "github-actions[bot]" - and "🔒 Automated Security Review" in comment.body + and "Automated Security Review" in comment.body ): return comment return None @@ -149,7 +153,7 @@ def post_or_update_comment(body: str): def handle_tool_call(name: str, inputs: dict) -> str: if name == "post_security_review": # Prepend a header to identify review comments across runs - body = f"## 🔒 Automated Security Review\n\n{inputs['body']}" + body = f"## Automated Security Review\n\n{inputs['body']}" post_or_update_comment(body) return "Security review comment posted." return f"Unknown tool: {name}" @@ -184,6 +188,7 @@ def run_security_review_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py index f5fdbb18d..1f68182a9 100644 --- a/scripts/triage_agent.py +++ b/scripts/triage_agent.py @@ -116,21 +116,23 @@ # System prompt SYSTEM_PROMPT = """You are an issue triage assistant for a GitHub repository. -Given a new issue and a list of existing open issues, you must: - -1. Check whether the new issue is a duplicate of an existing one. - - If it clearly is the same issue, call mark_duplicate and stop — do not label or acknowledge further. - - If it seems related but could be distinct, call suggest_possible_duplicate. That comment - will serve as the acknowledgment too, so do NOT post a separate acknowledgment afterward. -2. Otherwise, classify it by applying appropriate labels - (bug, feature-request, question, documentation, needs-info, good-first-issue). -3. If the issue is missing key info (steps to reproduce for bugs, use case for features, etc.), - post a friendly comment asking for it. -4. If no possible duplicate was flagged, post a short acknowledgment comment so the - author knows their issue was received. Do NOT post comments on administrative issues - such as meeting minutes, roadmaps, etc. - -Keep comments concise and friendly.""" +Given a new issue and a list of existing open issues, follow these steps in order. +No emojis. + +1. DUPLICATE CHECK: If the issue clearly duplicates an existing one, call mark_duplicate and stop. + If it seems related but distinct, call suggest_possible_duplicate and continue triage. +2. LABEL: Apply appropriate labels (bug, enhancement, question, documentation, needs-info, good-first-issue, etc.). +3. NEEDS INFO: If the issue lacks key details (reproduction steps for bugs, use case for features), post a comment asking for them using this format: + +Thanks for opening this issue. To help us investigate, please provide: +- +... (repeat for each missing detail) + +4. ACKNOWLEDGE: If no duplicate was flagged and no needs-info comment was posted, acknowledge receipt with this format: + +Thanks for the report. We will take a look. + +Do not post acknowledgments on administrative issues such as meeting minutes or roadmaps.""" # GitHub helpers @@ -172,9 +174,9 @@ def post_comment(body: str) -> str: def mark_duplicate(original_issue_number: int, reason: str) -> str: original = repo.get_issue(original_issue_number) issue.create_comment( - f"Thanks for the report! This looks like a duplicate of #{original_issue_number} " + f"This looks like a duplicate of #{original_issue_number} " f"({original.html_url}).\n\n> {reason}\n\n" - f"Please edit this issue to add any distinguishing details if you believe it's not a duplicate." + f"If you believe it is distinct, please edit this issue with any additional details." ) issue.add_to_labels("duplicate") return f"Marked as duplicate of #{original_issue_number}." @@ -183,9 +185,9 @@ def mark_duplicate(original_issue_number: int, reason: str) -> str: def suggest_possible_duplicate(related_issue_number: int, reason: str) -> str: related = repo.get_issue(related_issue_number) issue.create_comment( - f"Hey! This might be related to #{related_issue_number} " - f"({related.html_url}) — {reason}\n\n" - f"Feel free to check if that one already covers what you're reporting!" + f"This may be related to #{related_issue_number} " + f"({related.html_url}): {reason}\n\n" + f"Please check if that issue already covers what you are reporting." ) return f"Flagged as possibly related to #{related_issue_number}." @@ -230,6 +232,7 @@ def run_triage_agent(): model=MODEL, messages=messages, tools=TOOLS, + temperature=0, ) message = response.choices[0].message From 54c73c599b9d711071a215378ce0b888048cf762 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 13:43:35 +0900 Subject: [PATCH 4/7] refactor: extract configurable params to workflow file --- .github/workflows/pr-quality-check.yml | 10 +++++----- .github/workflows/security-review.yml | 9 ++++++--- .github/workflows/triage.yml | 10 ++++++---- scripts/security_review_agent.py | 24 ++++++++++-------------- scripts/triage_agent.py | 8 +++----- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 8ccaaa6ea..94961cee5 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -20,14 +20,14 @@ jobs: MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions - AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} + AUTHOR_USERNAME: ${{ github.event.pull_request.user.login }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BODY: ${{ github.event.pull_request.body }} PR_NUMBER: ${{ github.event.pull_request.number }} - REPO_NAME: ${{ github.repository }} PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} + REPO_NAME: ${{ github.repository }} run: python scripts/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 38c5194c4..4b0a55d5e 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -28,15 +28,18 @@ jobs: - run: pip install litellm PyGithub - name: Run security review agent env: + IGNORED_EXTENSIONS: .lock,.sum + IGNORED_FILENAMES: package-lock.json,yarn.lock,poetry.lock,Gemfile.lock,Cargo.lock,composer.lock,pnpm-lock.yaml,pip.lock + MAX_PATCH_CHARS_PER_FILE: 3000 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions - REPO_NAME: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + REPO_NAME: ${{ github.repository }} TRIGGER: ${{ github.event_name }} run: python scripts/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index b52bb72a0..7359d2fc5 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -16,16 +16,18 @@ jobs: - run: pip install litellm PyGithub - name: Run triage agent env: + AVAILABLE_LABELS: automation,bug,dependencies,documentation,enhancement,good-first-issue,meeting,needs-info,plugins,protocol,question,security,tech-debt,testing + LATEST_ISSUES_LIMIT: 100 # e.g: "claude-sonnet-4-6", "gpt-4o", etc. MODEL: ${{ secrets.MODEL }} # Only API key for the chosen model is required ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Obtained automatically by GH Actions + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_BODY: ${{ github.event.issue.body }} ISSUE_NUMBER: ${{ github.event.issue.number }} - REPO_NAME: ${{ github.repository }} ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} + REPO_NAME: ${{ github.repository }} run: python scripts/triage_agent.py diff --git a/scripts/security_review_agent.py b/scripts/security_review_agent.py index 51ff803bf..d421d6ee4 100644 --- a/scripts/security_review_agent.py +++ b/scripts/security_review_agent.py @@ -18,23 +18,19 @@ if not any(os.environ.get(api_key) for api_key in valid_api_keys): raise ValueError("No API key is set") +IGNORED_FILENAMES = set(os.environ.get( + "IGNORED_FILENAMES", + "package-lock.json,yarn.lock,poetry.lock,Gemfile.lock,Cargo.lock,composer.lock,pnpm-lock.yaml,pip.lock" +).split(",")) -# Exclude files that are not useful for security analysis -IGNORED_FILENAMES = { - "package-lock.json", - "yarn.lock", - "poetry.lock", - "Gemfile.lock", - "Cargo.lock", - "composer.lock", - "pnpm-lock.yaml", - "pip.lock", -} - -IGNORED_EXTENSIONS = {".lock", ".sum"} +# Extensions must include a leading dot +IGNORED_EXTENSIONS = set(os.environ.get( + "IGNORED_EXTENSIONS", + ".lock,.sum" +).split(",")) # Truncate very large diffs like generated files to prevent bloating the prompt -MAX_PATCH_CHARS_PER_FILE = 3000 +MAX_PATCH_CHARS_PER_FILE = int(os.environ.get("MAX_PATCH_CHARS_PER_FILE", 3000)) # System prompt diff --git a/scripts/triage_agent.py b/scripts/triage_agent.py index 1f68182a9..88fd911c2 100644 --- a/scripts/triage_agent.py +++ b/scripts/triage_agent.py @@ -9,7 +9,8 @@ repo = gh.get_repo(os.environ["REPO_NAME"]) issue = repo.get_issue(int(os.environ["ISSUE_NUMBER"])) -LATEST_ISSUES_LIMIT = 100 +LATEST_ISSUES_LIMIT = int(os.environ["LATEST_ISSUES_LIMIT"], 100) +AVAILABLE_LABELS = os.environ.get("AVAILABLE_LABELS", "bug,enhancement,question,documentation,needs-info") MODEL = os.environ["MODEL"] for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: @@ -29,10 +30,7 @@ "name": "apply_label", "description": ( "Apply one or more labels to the issue. " - "Use labels like: automation, bug, dependencies, " - "documentation, enhancement, good-first-issue, " - "meeting, needs-info, plugins, protocol, question, " - "security, tech-debt, testing." + "Use labels like: " + AVAILABLE_LABELS ), "parameters": { "type": "object", From c652d04fb41da7b626b2ea07d0d5c4c8ef13673e Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:04:28 +0900 Subject: [PATCH 5/7] refactor: extract helper functions, move scripts to scripts/agents --- .github/workflows/pr-quality-check.yml | 2 +- .github/workflows/security-review.yml | 2 +- .github/workflows/triage.yml | 2 +- scripts/agents/helpers.py | 33 +++++++++++++++ scripts/{ => agents}/pr_checker_agent.py | 42 ++----------------- scripts/{ => agents}/security_review_agent.py | 42 ++----------------- scripts/{ => agents}/triage_agent.py | 38 ++--------------- 7 files changed, 48 insertions(+), 113 deletions(-) create mode 100644 scripts/agents/helpers.py rename scripts/{ => agents}/pr_checker_agent.py (76%) rename scripts/{ => agents}/security_review_agent.py (81%) rename scripts/{ => agents}/triage_agent.py (86%) diff --git a/.github/workflows/pr-quality-check.yml b/.github/workflows/pr-quality-check.yml index 94961cee5..2deda4c0b 100644 --- a/.github/workflows/pr-quality-check.yml +++ b/.github/workflows/pr-quality-check.yml @@ -30,4 +30,4 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} REPO_NAME: ${{ github.repository }} - run: python scripts/pr_checker_agent.py + run: python scripts/agents/pr_checker_agent.py diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 4b0a55d5e..47dfb548c 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -42,4 +42,4 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} REPO_NAME: ${{ github.repository }} TRIGGER: ${{ github.event_name }} - run: python scripts/security_review_agent.py + run: python scripts/agents/security_review_agent.py diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 7359d2fc5..6188ba3ca 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -30,4 +30,4 @@ jobs: ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} REPO_NAME: ${{ github.repository }} - run: python scripts/triage_agent.py + run: python scripts/agents/triage_agent.py diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py new file mode 100644 index 000000000..fdafb5198 --- /dev/null +++ b/scripts/agents/helpers.py @@ -0,0 +1,33 @@ +def validate_api_keys(): + valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] + if not any(os.environ.get(k) for k in valid_api_keys): + raise ValueError("No API key is set") + + +def validate_env_vars(env_vars: list[str]): + for env_var in env_vars: + if not os.environ.get(env_var): + raise ValueError(f"{env_var} is not set") + + +def run_agent(messages: list, tools: list, handle_tool_call, model: str): + while True: + response = litellm.completion( + model=model, messages=messages, tools=tools, temperature=0 + ) + message = response.choices[0].message + if message.content: + print(f"[agent] {message.content}") + messages.append(message.model_dump(exclude_none=True)) + if response.choices[0].finish_reason == "stop" or not message.tool_calls: + break + tool_results = [] + for tool_call in message.tool_calls: + inputs = json.loads(tool_call.function.arguments) + result = handle_tool_call(tool_call.function.name, inputs) + tool_results.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + }) + messages.extend(tool_results) diff --git a/scripts/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py similarity index 76% rename from scripts/pr_checker_agent.py rename to scripts/agents/pr_checker_agent.py index 74c0cc12d..a4a4257f0 100644 --- a/scripts/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -11,14 +12,8 @@ author = os.environ["AUTHOR_USERNAME"] MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") - +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"]) +validate_api_keys() # Tools @@ -126,36 +121,7 @@ def run_pr_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - - if message.content: - print(f"[agent] {message.content}") - - messages.append(message.model_dump(exclude_none=True)) - - if response.choices[0].finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": diff --git a/scripts/security_review_agent.py b/scripts/agents/security_review_agent.py similarity index 81% rename from scripts/security_review_agent.py rename to scripts/agents/security_review_agent.py index d421d6ee4..43251bb7b 100644 --- a/scripts/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -10,13 +11,8 @@ pr = repo.get_pull(int(os.environ["PR_NUMBER"])) MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "MODEL"]) +validate_api_keys() IGNORED_FILENAMES = set(os.environ.get( "IGNORED_FILENAMES", @@ -178,37 +174,7 @@ def run_security_review_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - - if message.content: - print(f"[agent] {message.content}") - - messages.append(message.model_dump(exclude_none=True)) - - if response.choices[0].finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - print(f"[tool] {tool_call.function.name}: {result}") - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": diff --git a/scripts/triage_agent.py b/scripts/agents/triage_agent.py similarity index 86% rename from scripts/triage_agent.py rename to scripts/agents/triage_agent.py index 88fd911c2..aa2002e13 100644 --- a/scripts/triage_agent.py +++ b/scripts/agents/triage_agent.py @@ -2,6 +2,7 @@ import json import litellm from github import Github, Auth +from helpers import validate_env_vars, validate_api_keys, run_agent # Setup @@ -13,13 +14,8 @@ AVAILABLE_LABELS = os.environ.get("AVAILABLE_LABELS", "bug,enhancement,question,documentation,needs-info") MODEL = os.environ["MODEL"] -for env_var in ["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]: - if not os.environ[env_var]: - raise ValueError(f"{env_var} is not set") - -valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] -if not any(os.environ.get(api_key) for api_key in valid_api_keys): - raise ValueError("No API key is set") +validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "ISSUE_NUMBER", "ISSUE_TITLE", "ISSUE_BODY", "MODEL"]) +validate_api_keys() # Tools @@ -224,33 +220,7 @@ def run_triage_agent(): {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_initial_message()}, ] - - while True: - response = litellm.completion( - model=MODEL, - messages=messages, - tools=TOOLS, - temperature=0, - ) - - message = response.choices[0].message - messages.append(message.model_dump(exclude_none=True)) - - finish_reason = response.choices[0].finish_reason - if finish_reason == "stop" or not message.tool_calls: - break - - tool_results = [] - for tool_call in message.tool_calls: - inputs = json.loads(tool_call.function.arguments) - result = handle_tool_call(tool_call.function.name, inputs) - tool_results.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": result, - }) - - messages.extend(tool_results) + run_agent(messages, TOOLS, handle_tool_call, MODEL) if __name__ == "__main__": From b7da4016577f5d31eaa7b46600a53f0b0224c53d Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:21:35 +0900 Subject: [PATCH 6/7] fix: python imports and remove unused --- scripts/agents/helpers.py | 4 ++++ scripts/agents/pr_checker_agent.py | 2 -- scripts/agents/security_review_agent.py | 2 -- scripts/agents/triage_agent.py | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/agents/helpers.py b/scripts/agents/helpers.py index fdafb5198..0cda26e17 100644 --- a/scripts/agents/helpers.py +++ b/scripts/agents/helpers.py @@ -1,3 +1,7 @@ +import os +import json +import litellm + def validate_api_keys(): valid_api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"] if not any(os.environ.get(k) for k in valid_api_keys): diff --git a/scripts/agents/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py index a4a4257f0..52bbced9d 100644 --- a/scripts/agents/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index 43251bb7b..d60660715 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent diff --git a/scripts/agents/triage_agent.py b/scripts/agents/triage_agent.py index aa2002e13..366a4dbb6 100644 --- a/scripts/agents/triage_agent.py +++ b/scripts/agents/triage_agent.py @@ -1,6 +1,4 @@ import os -import json -import litellm from github import Github, Auth from helpers import validate_env_vars, validate_api_keys, run_agent From b544668e396847fe31f4ebdfbe18178f7217be52 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 17 Apr 2026 14:34:48 +0900 Subject: [PATCH 7/7] chore: add greeting and disclaimer to output --- scripts/agents/pr_checker_agent.py | 2 +- scripts/agents/security_review_agent.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/agents/pr_checker_agent.py b/scripts/agents/pr_checker_agent.py index 52bbced9d..ac2742e75 100644 --- a/scripts/agents/pr_checker_agent.py +++ b/scripts/agents/pr_checker_agent.py @@ -56,7 +56,7 @@ When posting a comment, always use this exact structure (omit sections that don't apply): - (first-time contributors only) +Thanks for the contribution! diff --git a/scripts/agents/security_review_agent.py b/scripts/agents/security_review_agent.py index d60660715..9a6c73723 100644 --- a/scripts/agents/security_review_agent.py +++ b/scripts/agents/security_review_agent.py @@ -52,6 +52,8 @@ ... (repeat for each finding) + +Disclaimer: This review is AI-generated. Please validate the findings before fixing. """ # GitHub helpers