Skip to content

Commit 6ebf83c

Browse files
committed
Merge branch 'dev'
2 parents e5d6b04 + cde8059 commit 6ebf83c

13 files changed

Lines changed: 284 additions & 130 deletions

app/github/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { handleGithubAuth } from "@lib/handleGithubAuth"
2+
import { replyIssueComment } from "@lib/replyIssueComment"
23
import { summarizePullRequest } from "@lib/summarizePullRequest"
34
import { NextRequest, NextResponse } from "next/server"
45

@@ -9,9 +10,17 @@ export async function POST(req: NextRequest) {
910

1011
try {
1112
if (payload.action == "opened" || payload.action == "synchronize") {
13+
// If a PR is opened or updated, summarize it
1214
const octokit = await handleGithubAuth(payload)
1315

1416
await summarizePullRequest(payload, octokit)
17+
} else if (payload.action == "created") {
18+
if (payload.comment.body.includes("/ask-codex")) {
19+
// If a comment is created, reply to it
20+
const octokit = await handleGithubAuth(payload)
21+
22+
await replyIssueComment(payload, octokit)
23+
}
1524
}
1625

1726
return NextResponse.json("ok")

lib/joinStringsUntilMaxLength.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
export function joinStringsUntilMaxLength(
22
parsedFiles: string[],
33
maxLength: number
4-
): string {
5-
let combinedString = ""
4+
) {
5+
let codeDiff = ""
66
let currentLength = 0
7+
let maxLengthExceeded = false
78

89
for (const file of parsedFiles) {
910
const fileLength = file.length
1011

1112
if (currentLength + fileLength <= maxLength) {
12-
combinedString += file
13+
codeDiff += file
1314
currentLength += fileLength
1415
} else {
16+
maxLengthExceeded = true
1517
const remainingLength = maxLength - currentLength
16-
combinedString += file.slice(0, remainingLength)
18+
codeDiff += file.slice(0, remainingLength)
1719
break
1820
}
1921
}
2022

21-
return combinedString
23+
return { codeDiff, maxLengthExceeded }
2224
}

lib/replyIssueComment.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Octokit } from "@octokit/rest"
2+
import { ChatCompletionRequestMessage } from "openai-streams"
3+
import { generateChatGpt } from "../utils/generateChatGpt"
4+
import { getCodeDiff } from "../utils/getCodeDiff"
5+
6+
export const startDescription = "\n\n<!-- start pr-codex -->"
7+
export const endDescription = "<!-- end pr-codex -->"
8+
const systemPrompt =
9+
"You are a Git diff assistant. Given a code diff, you answer any question related to it. Be concise. Always wrap file names, functions, objects and similar in backticks (`)."
10+
11+
export async function replyIssueComment(payload: any, octokit: Octokit) {
12+
// Get relevant PR information
13+
const { repository, issue, sender, comment } = payload
14+
15+
const question = comment.body.split("/ask-codex")[1].trim()
16+
17+
if (question) {
18+
const { owner, repo, issue_number } = {
19+
owner: repository.owner.login,
20+
repo: repository.name,
21+
issue_number: issue.number
22+
}
23+
24+
// Get the diff content using Octokit and GitHub API
25+
const { codeDiff } = await getCodeDiff(owner, repo, issue_number, octokit)
26+
27+
// If there are changes, trigger workflow
28+
if (codeDiff?.length != 0) {
29+
const messages: ChatCompletionRequestMessage[] = [
30+
{
31+
role: "system",
32+
content: `${systemPrompt}\n\nHere is the code diff:\n\n${codeDiff}`
33+
},
34+
{
35+
role: "user",
36+
content: `${question}`
37+
}
38+
]
39+
40+
const codexResponse = await generateChatGpt(messages)
41+
42+
const description = `> ${question}\n\n@${sender.login} ${codexResponse}`
43+
44+
await octokit.issues.createComment({
45+
owner,
46+
repo,
47+
issue_number,
48+
body: description
49+
})
50+
51+
return codexResponse
52+
}
53+
throw new Error("No changes in PR")
54+
}
55+
}

lib/summarizePullRequest.ts

Lines changed: 38 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,81 @@
11
import { Octokit } from "@octokit/rest"
2-
import { ChatCompletionRequestMessage, OpenAI } from "openai-streams"
3-
import { yieldStream } from "yield-stream"
4-
import { parseDiff } from "../utils/parseDiff"
5-
import { joinStringsUntilMaxLength } from "./joinStringsUntilMaxLength"
2+
import { ChatCompletionRequestMessage } from "openai-streams"
3+
import { generateChatGpt } from "../utils/generateChatGpt"
4+
import { getCodeDiff } from "../utils/getCodeDiff"
65

7-
export const startDescription = "<!-- start pr-codex -->"
6+
export const startDescription = "\n\n<!-- start pr-codex -->"
87
export const endDescription = "<!-- end pr-codex -->"
8+
const systemPrompt =
9+
"You are a Git diff assistant. Given a code diff, you provide a clear and concise description of its content. Always wrap file names, functions, objects and similar in backticks (`)."
910

1011
export async function summarizePullRequest(payload: any, octokit: Octokit) {
1112
// Get relevant PR information
1213
const pr = payload.pull_request
13-
const { owner, repo, number } = {
14+
const { owner, repo, pull_number } = {
1415
owner: pr.base.repo.owner.login,
1516
repo: pr.base.repo.name,
16-
number: pr.number
17+
pull_number: pr.number
1718
}
1819

1920
// Get the diff content using Octokit and GitHub API
20-
const compareResponse = await octokit.rest.repos.compareCommits({
21+
const { codeDiff, skippedFiles, maxLengthExceeded } = await getCodeDiff(
2122
owner,
2223
repo,
23-
base: pr.base.sha,
24-
head: pr.head.sha,
25-
mediaType: {
26-
format: "diff"
27-
}
28-
})
29-
const diffContent = String(compareResponse.data)
30-
31-
// Parses the diff content and returns the parsed files.
32-
// If the number of changes in a file is greater than 1k changes, the file will be skipped.
33-
// The codeDiff is the joined string of parsed files, up to a max length of 10k.
34-
const maxChanges = 1000
35-
const { parsedFiles, skippedFiles } = parseDiff(diffContent, maxChanges)
36-
const codeDiff = joinStringsUntilMaxLength(parsedFiles, 10000)
24+
pull_number,
25+
octokit
26+
)
3727

3828
// If there are changes, trigger workflow
39-
if (codeDiff.length != 0) {
40-
const systemPrompt = `You are a Git diff assistant. Always begin with "This PR". Given a code diff, you provide a simple description in prose, in less than 300 chars, which sums up the changes. Continue with "\n\n### Detailed summary\n" and make a comprehensive list of all changes, excluding any eventual skipped files. Be concise. Always wrap file names, functions, objects and similar in backticks (\`).${
41-
skippedFiles.length != 0
42-
? ` After the list, conclude with "\n\n> " and mention that the following files were skipped due to too many changes: ${skippedFiles.join(
43-
","
44-
)}.`
45-
: ""
46-
}`
47-
29+
if (codeDiff?.length != 0) {
4830
const messages: ChatCompletionRequestMessage[] = [
4931
{
5032
role: "system",
51-
content: systemPrompt
33+
content: `${systemPrompt}\n\nHere is the code diff:\n\n${codeDiff}`
5234
},
5335
{
5436
role: "user",
55-
content: `Here is the code diff:\n\n${codeDiff}`
37+
content:
38+
'Starting with "This PR", clearly explain the focus of this PR in prose, in less than 300 characters. Then follow up with "\n\n### Detailed summary\n" and make a comprehensive list of all changes.'
5639
}
5740
]
5841

59-
const summary = await generateChatGpt(messages)
42+
const codexResponse = await generateChatGpt(messages)
6043

6144
// Check if the PR already has a comment from the bot
6245
const hasCodexCommented =
6346
payload.action == "synchronize" &&
64-
pr.body?.split("\n\n" + startDescription).length > 1
65-
66-
// if (firstComment) {
67-
// // Edit pinned bot comment to the PR
68-
// await octokit.issues.updateComment({
69-
// owner,
70-
// repo,
71-
// comment_id: firstComment.id,
72-
// body: summary
73-
// })
74-
// } else {
75-
// // Add a comment to the PR
76-
// await octokit.issues.createComment({
77-
// owner,
78-
// repo,
79-
// issue_number: number,
80-
// body: summary
81-
// })
82-
// }
47+
pr.body?.split(startDescription).length > 1
8348

84-
const prCodexText = `\n\n${startDescription}\n\n---\n\n## PR-Codex overview\n${summary}\n\n${endDescription}`
49+
const prCodexText = `${startDescription}\n\n${
50+
(hasCodexCommented ? pr.body.split(startDescription)[0].trim() : pr.body)
51+
? "---\n\n"
52+
: ""
53+
}## PR-Codex overview\n${codexResponse}${
54+
skippedFiles.length != 0
55+
? `\n\n> The following files were skipped due to too many changes: ${skippedFiles.join(
56+
", "
57+
)}`
58+
: ""
59+
}${
60+
maxLengthExceeded
61+
? "\n\n> The code diff exceeds the max number of characters, so this overview may be incomplete."
62+
: ""
63+
}\n\n${endDescription}`
8564

8665
const description = hasCodexCommented
87-
? pr.body.split("\n\n" + startDescription)[0] +
66+
? pr.body.split(startDescription)[0] +
8867
prCodexText +
8968
pr.body.split(endDescription)[1]
90-
: pr.body + prCodexText
69+
: (pr.body ?? "") + prCodexText
9170

9271
await octokit.issues.update({
9372
owner,
9473
repo,
95-
issue_number: number,
74+
issue_number: pull_number,
9675
body: description
9776
})
9877

99-
return summary
100-
}
101-
}
102-
103-
const generateChatGpt = async (messages: ChatCompletionRequestMessage[]) => {
104-
const DECODER = new TextDecoder()
105-
let text = ""
106-
107-
try {
108-
const stream = await OpenAI(
109-
"chat",
110-
{
111-
model: "gpt-3.5-turbo",
112-
temperature: 0.7,
113-
messages
114-
},
115-
{ apiKey: process.env.OPENAI_API_KEY }
116-
)
117-
118-
for await (const chunk of yieldStream(stream)) {
119-
try {
120-
const decoded: string = DECODER.decode(chunk)
121-
122-
if (decoded === undefined)
123-
throw new Error(
124-
"No choices in response. Decoded response: " +
125-
JSON.stringify(decoded)
126-
)
127-
128-
text += decoded
129-
} catch (err) {
130-
console.error(err)
131-
}
132-
}
133-
} catch (err) {
134-
console.error(err)
78+
return codexResponse
13579
}
136-
137-
return text
80+
throw new Error("No changes in PR")
13881
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"start": "next start",
1010
"lint": "next lint",
1111
"prettier": "prettier --write . --ignore-path .gitignore",
12-
"summarize": "npx ts-node scripts/summarize"
12+
"summarize": "npx ts-node scripts/summarize",
13+
"reply": "npx ts-node scripts/reply"
1314
},
1415
"dependencies": {
1516
"@headlessui/react": "^1.7.14",

scripts/reply.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import dotenv from "dotenv"
2+
import { handleGithubAuth } from "../lib/handleGithubAuth"
3+
import { replyIssueComment } from "../lib/replyIssueComment"
4+
import { testPayloadComment } from "../utils/github/testPayloadComment"
5+
6+
dotenv.config()
7+
8+
// Customize payload in `utils/testPayloadComment`
9+
10+
async function main() {
11+
try {
12+
const octokit = await handleGithubAuth(testPayloadComment)
13+
14+
console.log("Generating comment...")
15+
16+
const comment = await replyIssueComment(testPayloadComment, octokit)
17+
18+
console.log(
19+
"PR-Codex commented:\n\n",
20+
comment,
21+
"\n\nView on Github: https://github.com/decentralizedlabs/pr-codex/pull/4"
22+
)
23+
} catch (error) {
24+
console.log(error)
25+
}
26+
}
27+
28+
main()
29+
.then(() => process.exit(0))
30+
.catch((error) => {
31+
console.error(error)
32+
process.exit(1)
33+
})

scripts/summarize.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import dotenv from "dotenv"
22
import { handleGithubAuth } from "../lib/handleGithubAuth"
33
import { summarizePullRequest } from "../lib/summarizePullRequest"
4-
import { testPayload } from "../utils/github/testPayload"
4+
import { testPayloadSyncPr } from "../utils/github/testPayloadSyncPr"
55

66
dotenv.config()
77

8-
// Customize payload in `utils/testPayload`
8+
// Customize payload in `utils/testPayloadSyncPr`
99

1010
async function main() {
11-
const octokit = await handleGithubAuth(testPayload)
12-
1311
try {
12+
const octokit = await handleGithubAuth(testPayloadSyncPr)
13+
1414
console.log("Generating summary...")
1515

16-
const summary = await summarizePullRequest(testPayload, octokit)
16+
const summary = await summarizePullRequest(testPayloadSyncPr, octokit)
1717

1818
console.log(
1919
"PR-Codex wrote:\n\n",

utils/generateChatGpt.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ChatCompletionRequestMessage, OpenAI } from "openai-streams"
2+
import { yieldStream } from "yield-stream"
3+
4+
export const generateChatGpt = async (
5+
messages: ChatCompletionRequestMessage[]
6+
) => {
7+
const DECODER = new TextDecoder()
8+
let text = ""
9+
10+
try {
11+
const stream = await OpenAI(
12+
"chat",
13+
{
14+
model: "gpt-3.5-turbo",
15+
temperature: 0.7,
16+
messages
17+
},
18+
{ apiKey: process.env.OPENAI_API_KEY }
19+
)
20+
21+
for await (const chunk of yieldStream(stream)) {
22+
try {
23+
const decoded: string = DECODER.decode(chunk)
24+
25+
if (decoded === undefined)
26+
throw new Error(
27+
"No choices in response. Decoded response: " +
28+
JSON.stringify(decoded)
29+
)
30+
31+
text += decoded
32+
} catch (err) {
33+
console.error(err)
34+
}
35+
}
36+
} catch (err) {
37+
console.error(err)
38+
}
39+
40+
return text
41+
}

0 commit comments

Comments
 (0)