|
1 | 1 | 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" |
6 | 5 |
|
7 | | -export const startDescription = "<!-- start pr-codex -->" |
| 6 | +export const startDescription = "\n\n<!-- start pr-codex -->" |
8 | 7 | 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 (`)." |
9 | 10 |
|
10 | 11 | export async function summarizePullRequest(payload: any, octokit: Octokit) { |
11 | 12 | // Get relevant PR information |
12 | 13 | const pr = payload.pull_request |
13 | | - const { owner, repo, number } = { |
| 14 | + const { owner, repo, pull_number } = { |
14 | 15 | owner: pr.base.repo.owner.login, |
15 | 16 | repo: pr.base.repo.name, |
16 | | - number: pr.number |
| 17 | + pull_number: pr.number |
17 | 18 | } |
18 | 19 |
|
19 | 20 | // Get the diff content using Octokit and GitHub API |
20 | | - const compareResponse = await octokit.rest.repos.compareCommits({ |
| 21 | + const { codeDiff, skippedFiles, maxLengthExceeded } = await getCodeDiff( |
21 | 22 | owner, |
22 | 23 | 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 | + ) |
37 | 27 |
|
38 | 28 | // 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) { |
48 | 30 | const messages: ChatCompletionRequestMessage[] = [ |
49 | 31 | { |
50 | 32 | role: "system", |
51 | | - content: systemPrompt |
| 33 | + content: `${systemPrompt}\n\nHere is the code diff:\n\n${codeDiff}` |
52 | 34 | }, |
53 | 35 | { |
54 | 36 | 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.' |
56 | 39 | } |
57 | 40 | ] |
58 | 41 |
|
59 | | - const summary = await generateChatGpt(messages) |
| 42 | + const codexResponse = await generateChatGpt(messages) |
60 | 43 |
|
61 | 44 | // Check if the PR already has a comment from the bot |
62 | 45 | const hasCodexCommented = |
63 | 46 | 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 |
83 | 48 |
|
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}` |
85 | 64 |
|
86 | 65 | const description = hasCodexCommented |
87 | | - ? pr.body.split("\n\n" + startDescription)[0] + |
| 66 | + ? pr.body.split(startDescription)[0] + |
88 | 67 | prCodexText + |
89 | 68 | pr.body.split(endDescription)[1] |
90 | | - : pr.body + prCodexText |
| 69 | + : (pr.body ?? "") + prCodexText |
91 | 70 |
|
92 | 71 | await octokit.issues.update({ |
93 | 72 | owner, |
94 | 73 | repo, |
95 | | - issue_number: number, |
| 74 | + issue_number: pull_number, |
96 | 75 | body: description |
97 | 76 | }) |
98 | 77 |
|
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 |
135 | 79 | } |
136 | | - |
137 | | - return text |
| 80 | + throw new Error("No changes in PR") |
138 | 81 | } |
0 commit comments