Skip to content
Open
33 changes: 33 additions & 0 deletions .github/workflows/pr-quality-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: PR Quality Check
on:
pull_request_target:
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 }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Obtained automatically by GH Actions
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 }}
PR_TITLE: ${{ github.event.pull_request.title }}
REPO_NAME: ${{ github.repository }}
run: python scripts/agents/pr_checker_agent.py
45 changes: 45 additions & 0 deletions .github/workflows/security-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Security Review
on:
pull_request_target:
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:
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 }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Obtained automatically by GH Actions
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/agents/security_review_agent.py
33 changes: 33 additions & 0 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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:
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 }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
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 }}
ISSUE_TITLE: ${{ github.event.issue.title }}
REPO_NAME: ${{ github.repository }}
run: python scripts/agents/triage_agent.py
37 changes: 37 additions & 0 deletions scripts/agents/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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):
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)
126 changes: 126 additions & 0 deletions scripts/agents/pr_checker_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import os
from github import Github, Auth
from helpers import validate_env_vars, validate_api_keys, run_agent

# 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"]
validate_env_vars(["GITHUB_TOKEN", "REPO_NAME", "PR_NUMBER", "AUTHOR_USERNAME", "MODEL"])
validate_api_keys()

# 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.
Check the following in order, then post at most one comment combining all concerns. If nothing needs flagging, stay silent.

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.

Rules:
- One comment maximum. Combine all concerns.
- Silence if everything is fine.
- Be constructive, not demanding.
- No emojis.

When posting a comment, always use this exact structure (omit sections that don't apply):

Thanks for the contribution!

<what is unclear and what to add>

<ask to link or create an issue>

<quote rule from CONTRIBUTING.md, then explain what needs to change>
... (repeat for each rule that is violated)"""

# 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()},
]
run_agent(messages, TOOLS, handle_tool_call, MODEL)


if __name__ == "__main__":
run_pr_review_agent()
Loading
Loading