Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reviewer agent to summarize PR. #54

Merged
merged 17 commits into from
Dec 5, 2023
Merged
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
57 changes: 46 additions & 11 deletions app/api/webhook/github/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {App} from '@octokit/app'
import maige from '~/agents/maige'
import reviewer from '~/agents/reviewer'
import {GITHUB} from '~/constants'
import prisma from '~/prisma'
import {stripe} from '~/stripe'
Expand Down Expand Up @@ -153,6 +154,48 @@ export const POST = async (req: Request) => {
/**
* Issue-related events. We care about new issues and comments.
*/
const {
comment,
issue,
sender: {login: sender},
installation: {id: instanceId}
} = payload

if (sender.includes('maige'))
return new Response('Comment by Maige', {status: 202})

if (comment && !comment.body.toLowerCase().includes('maige'))
return new Response('Irrelevant comment', {status: 202})

if (issue.pull_request) {
const {
pull_request: {diff_url: diffUrl},
node_id: pullId
} = issue

// Get GitHub app instance access token
const app = new App({
appId: process.env.GITHUB_APP_ID || '',
privateKey: process.env.GITHUB_PRIVATE_KEY || ''
})

const octokit = await app.getInstallationOctokit(instanceId)

const response = await fetch(diffUrl)

if (!response.ok) return new Response('Could not fetch diff', {status: 401})

const data = await response.text()

await reviewer({
octokit: octokit,
input: `Instruction: ${comment?.body}\n\nPR Diff:\n${data}`,
pullId
})

return new Response('Reviewed PR', {status: 200})
}

if (
!(
(action === 'opened' && payload?.issue) ||
Expand All @@ -173,18 +216,9 @@ export const POST = async (req: Request) => {
node_id: repoId,
name,
owner: {login: owner}
},
sender: {login: sender},
installation: {id: instanceId},
comment
}
} = payload

if (sender.includes('maige'))
return new Response('Comment by Maige', {status: 202})

if (comment && !comment.body.toLowerCase().includes('maige'))
return new Response('Irrelevant comment', {status: 202})

const existingLabelNames = existingLabels?.map((l: Label) => l.name)

const customer = await prisma.customer.findUnique({
Expand Down Expand Up @@ -344,7 +378,8 @@ ${isComment ? `Comment by @${comment.user.login}: ${comment?.body}.` : ''}
octokit,
prisma,
customerId,
repoName: name
repoName: `${owner}/${name}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only downstream effect was in prisma.project.update({ where: { ... name: repoName } ... }) in the updateInstructions tool

allLabels
})

return new Response('ok', {status: 200})
Expand Down
Binary file modified bun.lockb
Binary file not shown.
36 changes: 28 additions & 8 deletions lib/agents/engineer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import {SerpAPI} from 'langchain/tools'
import env from '~/env.mjs'
import commentTool from '~/tools/comment'
import execTool from '~/tools/exec'
import githubTool from '~/tools/github'
import updateInstructionsTool from '~/tools/updateInstructions'
import {isDev} from '~/utils'

const model = new ChatOpenAI({
modelName: 'gpt-4-1106-preview',
openAIApiKey: env.OPENAI_API_KEY,
temperature: 0.7
temperature: 0.3
})

export default async function engineer({
Expand All @@ -30,16 +29,34 @@ export default async function engineer({
}) {
const shell = await Sandbox.create({
apiKey: env.E2B_API_KEY,
id: 'Nodejs',
onStderr: data => console.error(data.line),
onStdout: data => console.log(data.line)
})

function preCmdCallback(cmd: string) {
const tokenB64 = btoa(`pat:${env.GITHUB_ACCESS_TOKEN}`)
const authFlag = `-c http.extraHeader="AUTHORIZATION: basic ${tokenB64}"`

// Replace only first occurrence to avoid prompt injection
// Otherwise "git log && echo 'git '" would print the token
return cmd.replace('git ', `git ${authFlag} `)
}

const cloneName = `maige-${repoName.split('/')[1]}`

const repoSetup = preCmdCallback(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these changes be in the scope of this PR?

`git config --global user.email "${env.GITHUB_EMAIL}" && git config --global user.name "${env.GITHUB_USERNAME}" && git clone https://github.com/${repoName}.git ${cloneName} && cd ${cloneName} && git log -n 3`
)

const clone = await shell.process.start({
cmd: repoSetup
})
await clone.wait()

const tools = [
new SerpAPI(),
commentTool({octokit}),
updateInstructionsTool({octokit, prisma, customerId, repoName}),
githubTool({octokit}),
execTool({
name: 'shell',
description: 'Executes a shell command.',
Expand All @@ -48,7 +65,7 @@ export default async function engineer({
execTool({
name: 'git',
description:
'Executes a shell command with git logged in. Commands must begin with "git ".',
'Executes a shell command with git already logged in and configured. Commands must begin with "git ".',
setupCmd: `git config --global user.email "${env.GITHUB_EMAIL}" && git config --global user.name "${env.GITHUB_USERNAME}"`,
preCmdCallback: (cmd: string) => {
const tokenB64 = btoa(`pat:${env.GITHUB_ACCESS_TOKEN}`)
Expand All @@ -64,16 +81,19 @@ export default async function engineer({

const prefix = `You are a senior AI engineer.
You use the internet, shell, and git to solve problems.
You like to read the docs.
Only use necessary tools.
A shell has been initialized for your session and has a file system.
The repo has already been cloned and you can use ls or cat to view files, touch, mkdir, echo, etc.
Your job is to write code, commit it to a new branch, and open a pull request.
Always commit code before terminating.
YOUR FIRST STEP SHOULD ALWAYS BE TO RUN ls
{agent_scratchpad}
`.replaceAll('\n', ' ')

const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentType: 'openai-functions',
returnIntermediateSteps: isDev,
handleParsingErrors: true,
verbose: isDev,
verbose: false,
agentArgs: {
prefix
}
Expand Down
11 changes: 7 additions & 4 deletions lib/agents/maige.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {SerpAPI} from 'langchain/tools'
import env from '~/env.mjs'
import {codebaseSearch} from '~/tools/codeSearch'
import commentTool from '~/tools/comment'
import githubTool from '~/tools/github'
import {labelTool} from '~/tools/label'
import updateInstructionsTool from '~/tools/updateInstructions'
import {isDev} from '~/utils'

Expand All @@ -19,20 +19,23 @@ export default async function maige({
octokit,
prisma,
customerId,
repoName
repoName,
allLabels
}: {
input: string
octokit: any
prisma: any
customerId: string
repoName: string
allLabels: any[]
}) {
const tools = [
new SerpAPI(),
commentTool({octokit}),
updateInstructionsTool({octokit, prisma, customerId, repoName}),
githubTool({octokit}),
labelTool({octokit, allLabels}),
codebaseSearch({customerId, repoName})
// dispatchEngineer({octokit, prisma, customerId, repoName})
]

const prefix = `
Expand All @@ -46,7 +49,7 @@ You also maintain a set of user instructions that can customize your behaviour;
agentType: 'openai-functions',
returnIntermediateSteps: isDev,
handleParsingErrors: true,
verbose: isDev,
verbose: false,
agentArgs: {
prefix
}
Expand Down
47 changes: 47 additions & 0 deletions lib/agents/reviewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {initializeAgentExecutorWithOptions} from 'langchain/agents'
import {ChatOpenAI} from 'langchain/chat_models/openai'
import {SerpAPI} from 'langchain/tools'
import env from '~/env.mjs'
import {prComment} from '~/tools/prComment'
import {isDev} from '~/utils'

const model = new ChatOpenAI({
modelName: 'gpt-4-1106-preview',
openAIApiKey: env.OPENAI_API_KEY,
temperature: 0.3
})

export default async function reviewer({
input,
octokit,
pullId
}: {
input: string
octokit: any
pullId: string
}) {
const tools = [new SerpAPI(), prComment({octokit, pullId})]

const prefix = `
You are senior engineer reviewing a Pull Request in GitHub made by a junior engineer.
You MUST leave a comment on the PR according to the user's instructions using the prComment function.
Format your answer beautifully using markdown suitable for GitHub.
DO NOT use any emojis or non-Ascii characters.
{agent_scratchpad}
`.replaceAll('\n', ' ')

const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentType: 'openai-functions',
returnIntermediateSteps: isDev,
handleParsingErrors: true,
verbose: false,
agentArgs: {
prefix
}
})

const result = await executor.call({input})
const {output} = result

return output
}
39 changes: 39 additions & 0 deletions lib/tools/dispatchEngineer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {DynamicStructuredTool} from 'langchain/tools'
import {z} from 'zod'
import engineer from '~/agents/engineer'
/**
* Execute a shell command
*/
export default function dispatchEngineer({
octokit,
prisma,
customerId,
repoName
}: {
octokit: any
prisma: any
customerId: string
repoName: string
}) {
return new DynamicStructuredTool({
description: 'Dispatch an engineer to work on an issue',
func: async ({input}) => {
console.log('DISPATCHING ENGINEER')
engineer({
input,
octokit,
prisma,
customerId,
repoName
})
// await new Promise(resolve => setTimeout(resolve, 1000))
return 'dispatched'
},
name: 'dispatchEngineer',
schema: z.object({
input: z
.string()
.describe('specific, detailed instructions for the engineer.')
})
})
}
2 changes: 1 addition & 1 deletion lib/tools/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {z} from 'zod'
/**
* Call the GitHub REST API
*/
export default function github({octokit}: {octokit: any}) {
export function githubTool({octokit}: {octokit: any}) {
return new DynamicStructuredTool({
description: 'GitHub REST API',
func: async ({path, body, method}) => {
Expand Down
2 changes: 1 addition & 1 deletion lib/tools/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {labelIssue} from '~/utils/github'
/**
* Label an issue using GitHub REST API tool
*/
export default function label({
export function labelTool({
octokit,
allLabels
}: {
Expand Down
29 changes: 29 additions & 0 deletions lib/tools/prComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {DynamicStructuredTool} from 'langchain/tools'
import {z} from 'zod'
import {addComment} from '~/utils/github'

/**
* Comment on an issue
*/
export function prComment({octokit, pullId}: {octokit: any; pullId: string}) {
return new DynamicStructuredTool({
description: 'Adds a comment to a PR',
func: async ({comment, severity}) => {
const footer = `By [Maige](https://maige.app). How's my driving?`
const res = await addComment({
octokit,
issueId: pullId,
comment: `${comment}\nThis is a Level ${severity} problem.\n\n${footer}`
})
return JSON.stringify(res)
},
name: 'prComment',
schema: z.object({
comment: z.string().describe('The comment to add'),
severity: z
.number()
.optional()
.describe('The severity of the needed fix on a scale of 1-5')
})
})
}
2 changes: 1 addition & 1 deletion lib/tools/updateInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function updateInstructions({
where: {
customerId_name: {
customerId,
name: repoName
name: repoName.split('/')[1]
}
},
data: {
Expand Down
Loading