AI Bot #10
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: AI Bot | |
on: | |
issues: | |
types: [opened] | |
issue_comment: | |
types: [created] | |
pull_request_review_comment: | |
types: [created] | |
pull_request: | |
types: [opened, edited, synchronize] | |
jobs: | |
respond-to-commands: | |
runs-on: ubuntu-latest | |
if: | | |
(github.actor == 'f') && | |
((github.event_name == 'issues' && contains(github.event.issue.body, '/ai')) || | |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) || | |
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) || | |
(github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/ai'))) | |
permissions: | |
contents: write | |
pull-requests: write | |
issues: write | |
steps: | |
- uses: actions/checkout@v3 | |
with: | |
fetch-depth: 0 | |
- name: Setup Node.js | |
uses: actions/setup-node@v3 | |
with: | |
node-version: '18' | |
- name: Install dependencies | |
run: npm install openai@^4.0.0 @octokit/rest@^19.0.0 | |
- name: Process command | |
id: process | |
env: | |
GH_SECRET: ${{ secrets.GH_SECRET }} | |
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
run: | | |
node << 'EOF' | |
const OpenAI = require('openai'); | |
const { Octokit } = require('@octokit/rest'); | |
async function main() { | |
const openai = new OpenAI({ | |
apiKey: process.env.OPENAI_API_KEY | |
}); | |
const octokit = new Octokit({ | |
auth: process.env.GH_SECRET | |
}); | |
const eventName = process.env.GITHUB_EVENT_NAME; | |
const eventPath = process.env.GITHUB_EVENT_PATH; | |
const event = require(eventPath); | |
// Double check user authorization | |
const actor = event.sender?.login || event.pull_request?.user?.login || event.issue?.user?.login; | |
if (actor !== 'f') { | |
console.log('Unauthorized user attempted to use the bot:', actor); | |
return; | |
} | |
// Get command and context | |
let command = ''; | |
let issueNumber = null; | |
let isPullRequest = false; | |
if (eventName === 'issues') { | |
command = event.issue.body; | |
issueNumber = event.issue.number; | |
} else if (eventName === 'issue_comment') { | |
command = event.comment.body; | |
issueNumber = event.issue.number; | |
isPullRequest = !!event.issue.pull_request; | |
} else if (eventName === 'pull_request_review_comment') { | |
command = event.comment.body; | |
issueNumber = event.pull_request.number; | |
isPullRequest = true; | |
} else if (eventName === 'pull_request') { | |
command = event.pull_request.body; | |
issueNumber = event.pull_request.number; | |
isPullRequest = true; | |
} | |
if (!command.startsWith('/ai')) { | |
return; | |
} | |
// Extract the actual command after /ai | |
const aiCommand = command.substring(3).trim(); | |
// Handle resolve conflicts command | |
if (aiCommand === 'resolve' || aiCommand === 'fix conflicts') { | |
if (!isPullRequest) { | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '❌ The resolve command can only be used on pull requests.' | |
}); | |
return; | |
} | |
// Get PR files | |
const { data: files } = await octokit.pulls.listFiles({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber | |
}); | |
let readmeContent = ''; | |
let csvContent = ''; | |
let readmeChanged = false; | |
let csvChanged = false; | |
let newPrompt = ''; | |
let actName = ''; | |
// Analyze changes to extract prompt information | |
for (const file of files) { | |
if (file.filename === 'README.md' && (file.status === 'modified' || file.status === 'added')) { | |
readmeChanged = true; | |
const patch = file.patch || ''; | |
// Look for added lines in the patch | |
const addedLines = patch.split('\n') | |
.filter(line => line.startsWith('+')) | |
.map(line => line.substring(1)) | |
.join('\n'); | |
// Extract the new prompt section using the correct format | |
const promptMatch = addedLines.match(/## Act as (?:a |an )?([^\n]+)\n(?:Contributed by:[^\n]*\n)?(?:> )?([^#]+?)(?=\n\n|$)/); | |
if (promptMatch) { | |
actName = `Act as ${promptMatch[1].trim()}`; | |
newPrompt = promptMatch[2].trim(); | |
// Check if contributor line exists and is properly formatted | |
const contributorLine = addedLines.match(/Contributed by: \[@([^\]]+)\]\(https:\/\/github\.com\/([^\)]+)\)/); | |
if (!contributorLine) { | |
// If no contributor line or improperly formatted, add a comment about it | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '⚠️ Note: Contributor line is missing or improperly formatted. Please add it in the format:\nContributed by: [@username](https://github.com/username)' | |
}); | |
} | |
} | |
} | |
if (file.filename === 'prompts.csv' && (file.status === 'modified' || file.status === 'added')) { | |
csvChanged = true; | |
} | |
} | |
if (!readmeChanged && !csvChanged) { | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '❌ No changes found in README.md or prompts.csv' | |
}); | |
return; | |
} | |
if (!actName || !newPrompt) { | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '❌ Could not extract prompt information from README.md' | |
}); | |
return; | |
} | |
// Create a new branch for fixes | |
const branchName = `ai-bot/fix-conflicts-${issueNumber}`; | |
const defaultBranch = event.repository.default_branch; | |
// Get current branch ref | |
const { data: ref } = await octokit.git.getRef({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
ref: `heads/${defaultBranch}` | |
}); | |
try { | |
// Create new branch | |
await octokit.git.createRef({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
ref: `refs/heads/${branchName}`, | |
sha: ref.object.sha | |
}); | |
// If CSV wasn't updated, update it | |
if (!csvChanged) { | |
// Get current CSV content | |
const { data: currentCsv } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'prompts.csv', | |
ref: branchName | |
}); | |
// Add new prompt to CSV | |
const newCsvContent = Buffer.from(currentCsv.content, 'base64').toString('utf-8') + | |
`\n"${actName.replace(/"/g, '""')}","${newPrompt.replace(/"/g, '""')}"`; | |
// Update CSV file | |
await octokit.repos.createOrUpdateFileContents({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'prompts.csv', | |
message: `feat: Add "${actName}" to prompts.csv`, | |
content: Buffer.from(newCsvContent).toString('base64'), | |
branch: branchName, | |
sha: currentCsv.sha | |
}); | |
// Create PR or update existing one | |
await octokit.pulls.create({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
title: `feat: Add "${actName}" to prompts.csv`, | |
body: `This PR was automatically generated to sync prompts.csv with README.md changes.\n\nRelated to #${issueNumber}`, | |
head: branchName, | |
base: defaultBranch | |
}); | |
} | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '✨ Created a fix PR to sync README.md and prompts.csv' | |
}); | |
} catch (error) { | |
console.error('Error:', error); | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: `❌ Error while trying to fix conflicts: ${error.message}` | |
}); | |
} | |
return; | |
} | |
// Handle rename command specifically | |
if (aiCommand.startsWith('rename') || aiCommand === 'suggest title') { | |
if (!isPullRequest) { | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: '❌ The rename command can only be used on pull requests.' | |
}); | |
return; | |
} | |
// Get PR details for context | |
const { data: pr } = await octokit.pulls.get({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber | |
}); | |
// Get the list of files changed in the PR | |
const { data: files } = await octokit.pulls.listFiles({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber | |
}); | |
// Process file changes | |
const fileChanges = await Promise.all(files.map(async file => { | |
if (file.status === 'removed') { | |
return `Deleted: ${file.filename}`; | |
} | |
// Get file content for added or modified files | |
if (file.status === 'added' || file.status === 'modified') { | |
const patch = file.patch || ''; | |
return `${file.status === 'added' ? 'Added' : 'Modified'}: ${file.filename}\nChanges:\n${patch}`; | |
} | |
return `${file.status}: ${file.filename}`; | |
})); | |
const completion = await openai.chat.completions.create({ | |
model: "gpt-3.5-turbo", | |
messages: [ | |
{ | |
role: "system", | |
content: "You are a helpful assistant that generates clear and concise pull request titles. Follow these rules:\n1. Use conventional commit style (feat:, fix:, docs:, etc.)\n2. Focus on WHAT changed, not HOW or WHERE\n3. Keep it short and meaningful\n4. Don't mention file names or technical implementation details\n5. Return ONLY the new title, nothing else\n\nGood examples:\n- feat: Add \"Act as a Career Coach\"\n- fix: Correct typo in Linux Terminal prompt\n- docs: Update installation instructions\n- refactor: Improve error handling" | |
}, | |
{ | |
role: "user", | |
content: `Based on these file changes, generate a concise PR title:\n\n${fileChanges.join('\n\n')}` | |
} | |
], | |
temperature: 0.5, | |
max_tokens: 60 | |
}); | |
const newTitle = completion.choices[0].message.content.trim(); | |
// Update PR title | |
await octokit.pulls.update({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber, | |
title: newTitle | |
}); | |
// Add comment about the rename | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: `✨ Updated PR title to: "${newTitle}"\n\nBased on the following changes:\n\`\`\`diff\n${fileChanges.join('\n')}\n\`\`\`` | |
}); | |
return; | |
} | |
// Handle other commands | |
const completion = await openai.chat.completions.create({ | |
model: "gpt-3.5-turbo", | |
messages: [ | |
{ | |
role: "system", | |
content: "You are a helpful AI assistant that helps with GitHub repositories. You can suggest code changes, fix issues, and improve code quality." | |
}, | |
{ | |
role: "user", | |
content: aiCommand | |
} | |
], | |
temperature: 0.7, | |
max_tokens: 2000 | |
}); | |
const response = completion.choices[0].message.content; | |
// If response contains code changes, create a new branch and PR | |
if (response.includes('```')) { | |
const branchName = `ai-bot/fix-${issueNumber}`; | |
// Create new branch | |
const defaultBranch = event.repository.default_branch; | |
const ref = await octokit.git.getRef({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
ref: `heads/${defaultBranch}` | |
}); | |
await octokit.git.createRef({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
ref: `refs/heads/${branchName}`, | |
sha: ref.data.object.sha | |
}); | |
// Extract code changes and file paths from response | |
const codeBlocks = response.match(/```[\s\S]*?```/g); | |
for (const block of codeBlocks) { | |
const [_, filePath, ...codeLines] = block.split('\n'); | |
const content = Buffer.from(codeLines.join('\n')).toString('base64'); | |
await octokit.repos.createOrUpdateFileContents({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: filePath, | |
message: `AI Bot: Apply suggested changes for #${issueNumber}`, | |
content, | |
branch: branchName | |
}); | |
} | |
// Create PR | |
await octokit.pulls.create({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
title: `AI Bot: Fix for #${issueNumber}`, | |
body: `This PR was automatically generated in response to #${issueNumber}\n\nChanges proposed:\n${response}`, | |
head: branchName, | |
base: defaultBranch | |
}); | |
} | |
// Add comment with response | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: `AI Bot Response:\n\n${response}` | |
}); | |
} | |
main().catch(error => { | |
console.error('Error:', error); | |
process.exit(1); | |
}); | |
EOF | |
- name: Handle errors | |
if: failure() | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const issueNumber = context.issue.number || context.payload.pull_request.number; | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: issueNumber, | |
body: '❌ Sorry, there was an error processing your command. Please try again or contact the repository maintainers.' | |
}); |