AI Bot #14
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 README from main branch | |
const { data: readmeFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'README.md', | |
ref: 'main' | |
}); | |
// Get CSV from main branch | |
const { data: csvFile } = await octokit.repos.getContent({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
path: 'prompts.csv', | |
ref: 'main' | |
}); | |
// Prepare new README content | |
let readmeContent = Buffer.from(readmeFile.content, 'base64').toString('utf-8'); | |
const newSection = `\n## ${actName}\n${contributorInfo ? contributorInfo + '\n' : ''}\n> ${newPrompt}\n`; | |
readmeContent += newSection; | |
// Prepare new CSV content | |
let csvContent = Buffer.from(csvFile.content, 'base64').toString('utf-8'); | |
csvContent += `\n"${actName.replace(/"/g, '""')}","${newPrompt.replace(/"/g, '""')}"`; | |
// Update README in PR branch | |
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: readmeFile.sha | |
}); | |
// Update CSV in PR branch | |
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: csvFile.sha | |
}); | |
await octokit.issues.createComment({ | |
owner: event.repository.owner.login, | |
repo: event.repository.name, | |
issue_number: issueNumber, | |
body: `✨ Updated both files:\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.' | |
}); |