AI Bot #18
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; | |
} | |
try { | |
// Get PR details | |
const { data: pr } = await octokit.pulls.get({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber | |
}); | |
// Get the PR diff to extract the new prompt | |
const { data: files } = await octokit.pulls.listFiles({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
pull_number: issueNumber | |
}); | |
// Extract prompt from changes | |
let newPrompt = ''; | |
let actName = ''; | |
let contributorInfo = ''; | |
for (const file of files) { | |
if (file.filename === 'README.md') { | |
const patch = file.patch || ''; | |
const addedLines = patch.split('\n') | |
.filter(line => line.startsWith('+')) | |
.map(line => line.substring(1)) | |
.join('\n'); | |
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(); | |
const contributorLine = addedLines.match(/Contributed by: \[@([^\]]+)\]\(https:\/\/github\.com\/([^\)]+)\)/); | |
if (contributorLine) { | |
contributorInfo = `Contributed by: [@${contributorLine[1]}](https://github.com/${contributorLine[2]})`; | |
} | |
} | |
} | |
} | |
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 changes' | |
}); | |
return; | |
} | |
// Get content from main branch as reference | |
const { data: readmeFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'README.md', | |
ref: 'main' | |
}); | |
const { data: csvFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'prompts.csv', | |
ref: 'main' | |
}); | |
// Format the new prompt section | |
const newSection = `\n## ${actName}\n${contributorInfo ? contributorInfo + '\n' : ''}\n> ${newPrompt}\n`; | |
// Insert the new section before Contributors in README | |
let readmeContent = Buffer.from(readmeFile.content, 'base64').toString('utf-8'); | |
const contributorsIndex = readmeContent.indexOf('## Contributors'); | |
if (contributorsIndex === -1) { | |
readmeContent += newSection; // Append if Contributors section not found | |
} else { | |
readmeContent = readmeContent.slice(0, contributorsIndex) + newSection + readmeContent.slice(contributorsIndex); | |
} | |
// Get current files from PR branch to get their SHAs | |
const { data: prReadmeFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'README.md', | |
ref: pr.head.ref | |
}); | |
const { data: prCsvFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'prompts.csv', | |
ref: pr.head.ref | |
}); | |
// Update files in PR branch using PR's file SHAs | |
await octokit.repos.createOrUpdateFileContents({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'README.md', | |
message: `feat: Add "${actName}" to README`, | |
content: Buffer.from(readmeContent).toString('base64'), | |
branch: pr.head.ref, | |
sha: prReadmeFile.sha // Use PR's file SHA | |
}); | |
// Update CSV in PR branch | |
const csvContent = Buffer.from(csvFile.content, 'base64').toString('utf-8') + | |
`\n"${actName.replace(/"/g, '""')}","${newPrompt.replace(/"/g, '""')}"`; | |
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(csvContent).toString('base64'), | |
branch: pr.head.ref, | |
sha: prCsvFile.sha // Use PR's file SHA | |
}); | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: `✨ Updated files in your PR using main branch as base:\n1. Added "${actName}" to README.md\n2. Added the prompt to 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 update files: ${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.' | |
}); |