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
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