Files
e2b-dev--e2b/github/pullRequest.ts
T

351 lines
7.6 KiB
TypeScript

import { Configuration, OpenAIApi } from 'openai'
import { backOff } from 'exponential-backoff'
import api from 'api-client/api'
import { deployments, prisma } from 'db/prisma'
import { GitHubClient } from './client'
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
})
const openai = new OpenAIApi(configuration)
export async function prTitleFromInstructions(instructions: string) {
try {
const result = await backOff(() => openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
content: `${instructions}\n\nTl;dr in one sentence (around 50 characters) describing what to build. Don't use pronouns, just describe the thing:`,
role: 'user',
}
],
}))
return result.data.choices[0].message?.content
} catch (error: any) {
if (error.response) {
console.log(error.response.status)
console.log(error.response.data)
} else {
console.log(error.message)
}
}
}
// All PRs are also issues - GH API endpoint for issues is used for the shared functionality
export async function createPR({
client,
title,
body,
owner,
repo,
defaultBranch,
branch,
commitMessage,
}: {
commitMessage: string,
defaultBranch: string,
client: GitHubClient,
title: string,
body: string,
owner: string,
repo: string,
branch: string,
}) {
let baseBranchRefSHA: string | undefined
try {
const baseBranchRef = await client.git.getRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
})
baseBranchRefSHA = baseBranchRef.data.object.sha
} catch (error: any) {
// Make sure we are not overwriting an existing branch
// and only create a new branch and init repo if the repo is empty
if (error.message !== 'Git Repository is empty.') throw error
console.log('Repository is empty - creating initial empty commit')
// TODO: Create empty commit without placeholder file by deleting the file,
// creating an empty commit and then updating the default branch to point to the empty commit
// This behavious is not clearly documented and it seems that sometimes the empty commit can get automatically pruned before the ref can be updated
// Create placeholder file to create the default branch
console.log('Creating placeholder file')
const file = await client.repos.createOrUpdateFileContents({
owner,
repo,
branch: defaultBranch,
path: '/README.md',
message: 'Initial commit',
content: Buffer.from(`# ${repo}`, 'utf8').toString('base64'),
})
console.log('Finished creating placeholder file')
baseBranchRefSHA = file.data.commit.sha
}
const newBranchRef = await client.git.createRef({
owner,
repo,
ref: `refs/heads/${branch}`,
sha: baseBranchRefSHA!,
})
const currentCommit = await client.git.getCommit({
owner,
repo,
commit_sha: newBranchRef.data.object.sha,
})
const newCommit = await client.git.createCommit({
owner,
repo,
message: commitMessage,
tree: currentCommit.data.tree.sha,
parents: [currentCommit.data.sha],
})
await client.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommit.data.sha,
})
const pr = await client.pulls.create({
owner,
repo,
head: branch,
base: defaultBranch,
title,
body,
})
return {
issueID: pr.data.id,
number: pr.data.number,
url: pr.data.html_url,
}
}
export async function getPromptFromPR({
issueNumber,
client,
repo,
owner,
body,
}: {
body?: string,
owner: string,
repo: string,
client: GitHubClient,
// Number identifying the issue in the repo (this is not issueID)
issueNumber: number,
}): Promise<string> {
const [
issueResult,
commentsResult,
] = await Promise.all([
body === undefined ? client.issues.get({
issue_number: issueNumber,
owner,
repo,
}) : Promise.resolve(undefined),
client.issues.listComments({
issue_number: issueNumber,
owner,
repo,
// TODO: Handle pagination
page: 1,
per_page: 100,
}),
])
const prBody = issueResult?.data.body || body
const comments = commentsResult.data
.filter(c => c.user?.type !== 'Bot')
.map(c => c.body)
.join('\n')
return `${prBody}\n\n${comments}`
}
export async function getDeploymentsForPR({
installationID,
issueID,
repositoryID,
enabled,
}: {
enabled: boolean | undefined,
repositoryID: number,
issueID: number,
installationID: number,
}) {
return await prisma.deployments.findMany({
include: {
projects: {
include: {
teams: {
include: {
users_teams: {
include: {
users: true,
},
},
},
},
},
},
},
where: {
enabled,
AND: [
{
auth: {
path: ['github', 'repository_id'],
equals: repositoryID,
},
},
{
auth: {
path: ['github', 'issue_id'],
equals: issueID,
},
},
{
auth: {
path: ['github', 'installation_id'],
equals: installationID,
},
},
],
},
})
}
export const interactWithAgent = api
.path('/deployments/{id}/interactions')
.method('post')
.create()
export const createAgentDeployment = api
.path('/deployments')
.method('put')
.create({
project_id: true,
})
export const disableAgentDeployment = api
.path('/deployments/{id}')
.method('delete')
.create()
export async function triggerSmolDevAgentRun({
client,
deployment,
prompt,
accessToken,
commitMessage,
owner,
repo,
pullNumber,
}: {
pullNumber: number,
client: GitHubClient,
owner: string,
repo: string,
commitMessage: string,
deployment: deployments,
prompt: string,
accessToken: string,
}) {
console.log('Triggering smol dev agent run:', { owner, repo, pullNumber, prompt })
await addCommentToPR({
body: 'Started smol dev agent run',
client,
owner,
repo,
pullNumber,
})
await interactWithAgent({
id: deployment.id,
type: 'start',
data: {
instructions: {
// This provisioned access token has default expiry of 1 hour
AccessToken: accessToken,
CommitMessage: commitMessage,
Prompt: prompt,
GitHubAppName: (deployment?.auth as any)?.['github']?.['app_name'],
GitHubAppEmail: (deployment?.auth as any)?.['github']?.['app_email'],
Owner: owner,
Repo: repo,
Branch: (deployment?.auth as any)?.['github']?.['branch'],
},
} as any,
})
}
export async function getGHAccessToken({
client,
installationID,
repositoryID,
}: {
client: GitHubClient,
installationID: number,
repositoryID: number,
}) {
const result = await client.apps.createInstallationAccessToken({
installation_id: installationID,
repository_ids: [repositoryID],
permissions: {
contents: 'write',
},
})
return result.data.token
}
export async function getGHAppInfo({
client,
}: {
client: GitHubClient,
}) {
const result = await client.apps.getAuthenticated()
const id = result.data.id
const name = `${result.data.slug}[bot]`
return {
name,
email: `${id}+${name}@users.noreply.github.com`
}
}
export async function addCommentToPR({
pullNumber,
repo,
owner,
client,
body,
}: {
pullNumber: number,
repo: string,
owner: string,
client: GitHubClient,
body: string,
}) {
await client.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body,
})
}