diff --git a/README.md b/README.md index fa174ab6..700f0a18 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,16 @@ jobs: See also: [./action.yml](./action.yml) +### Conversation with OpenAI + +You can reply to a review comment made by this action and get a response based +on the diff context. Additionally, you can invite the bot to a conversation by +mentioning it in the beginning of the comment with `@openai`. + +Example: + +> @openai Can you please review this block of code? + #### Environment variables - `GITHUB_TOKEN`: This should already be available to the GitHub Action diff --git a/action.yml b/action.yml index 57ba104c..de5c6116 100644 --- a/action.yml +++ b/action.yml @@ -193,6 +193,83 @@ inputs: ```diff $patch ``` + comment_beginning: + required: false + description: 'Prompt for comment' + default: | + $system_message + + A comment was made on a review for a diff patch on file + `$filename`. You will be replying directly to that. If possible, I + will provide you the file and the entire diff to help + provide overall context. + + For context, the pull request has the title "$title" and the following + description: + + ``` + $description + ``` + + The OpenAI-generated summary is as follows: + + ``` + $summary + ``` + + Reply "OK" to confirm. + comment_file: + required: false + description: 'Prompt for file' + default: | + Here is the content of `$filename` for context. + + ``` + $file_content + ``` + comment_file_diff: + required: false + description: 'Prompt for file diff' + default: | + Here is the entire diff for `$filename` for context. + + ```diff + $file_diff + ``` + comment: + required: false + description: 'Prompt for comment' + default: | + I would like you to reply to the new comment made on + a conversation chain on a code review diff. + + Diff being commented on: + + ```diff + $diff + ``` + + The format of the conversation chain is: + `user: comment` + + Conversation chain (including the new comment): + + ``` + $comment_chain + ``` + + Please reply directly to the new comment in the conversation + chain without extra prose as that reply will be posted as-is. + + In your reply, please make sure to begin the reply by + tagging the user with "@user". + + The comment that you need to directly reply to: + + ``` + $comment + ``` + runs: using: 'node16' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 58c579e5..934554db 100644 --- a/dist/index.js +++ b/dist/index.js @@ -27048,6 +27048,7 @@ class Bot { /* harmony export */ __nccwpck_require__.d(__webpack_exports__, { /* harmony export */ "Es": () => (/* binding */ Commenter), +/* harmony export */ "Rp": () => (/* binding */ SUMMARIZE_TAG), /* harmony export */ "Rs": () => (/* binding */ COMMENT_TAG), /* harmony export */ "aD": () => (/* binding */ COMMENT_REPLY_TAG), /* harmony export */ "pK": () => (/* binding */ COMMENT_GREETING) @@ -27068,6 +27069,7 @@ const repo = context.repo; const COMMENT_GREETING = `:robot: OpenAI`; const COMMENT_TAG = ''; const COMMENT_REPLY_TAG = ''; +const SUMMARIZE_TAG = ''; const DESCRIPTION_TAG = ''; const DESCRIPTION_TAG_END = ''; class Commenter { @@ -27075,7 +27077,7 @@ class Commenter { * @param mode Can be "create", "replace", "append" and "prepend". Default is "replace". */ async comment(message, tag, mode) { - await comment(message, tag, mode); + await this.post_comment(message, tag, mode); } get_description(description) { // remove our summary from description by looking for description_tag and description_tag_end @@ -27106,10 +27108,9 @@ class Commenter { // if not found, add the tag and the content to the end of the description const tag_index = description.indexOf(DESCRIPTION_TAG); const tag_end_index = description.indexOf(DESCRIPTION_TAG_END); - const comment = `\n\n${DESCRIPTION_TAG}\n${message}\n${DESCRIPTION_TAG_END}`; + const comment = `${DESCRIPTION_TAG}\n${message}\n${DESCRIPTION_TAG_END}`; if (tag_index === -1 || tag_end_index === -1) { - let new_description = description; - new_description += comment; + const new_description = `${description}\n${comment}`; await octokit.pulls.update({ owner: repo.owner, repo: repo.repo, @@ -27141,7 +27142,7 @@ ${message} ${tag}`; // replace comment made by this action try { - const comments = await list_review_comments(pull_number); + const comments = await this.list_review_comments(pull_number); for (const comment of comments) { if (comment.path === path && comment.position === line) { // look for tag @@ -27172,11 +27173,11 @@ ${tag}`; } async getConversationChain(pull_number, comment) { try { - const reviewComments = await list_review_comments(pull_number); + const reviewComments = await this.list_review_comments(pull_number); const topLevelComment = await this.getTopLevelComment(reviewComments, comment); const conversationChain = reviewComments .filter((cmt) => cmt.in_reply_to_id === topLevelComment.id) - .map((cmt) => `${cmt.user.login}-(${cmt.id}): ${cmt.body}`); + .map((cmt) => `${cmt.user.login}: ${cmt.body}`); conversationChain.unshift(`${topLevelComment.user.login}: ${topLevelComment.body}`); return { chain: conversationChain.join('\n---\n'), @@ -27204,175 +27205,175 @@ ${tag}`; } return topLevelComment; } -} -const list_review_comments = async (target, page = 1) => { - const comments = []; - try { - let data; - do { - ; - ({ data } = await octokit.pulls.listReviewComments({ - owner: repo.owner, - repo: repo.repo, - pull_number: target, - page, - per_page: 100 - })); - comments.push(...data); - page++; - } while (data.length >= 100); - return comments; - } - catch (e) { - console.warn(`Failed to list review comments: ${e}`); - return comments; - } -}; -const comment = async (message, tag, mode) => { - let target; - if (context.payload.pull_request) { - target = context.payload.pull_request.number; - } - else if (context.payload.issue) { - target = context.payload.issue.number; - } - else { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: context.payload.pull_request and context.payload.issue are both null`); - return; - } - if (!tag) { - tag = COMMENT_TAG; + async list_review_comments(target, page = 1) { + const comments = []; + try { + let data; + do { + ; + ({ data } = await octokit.pulls.listReviewComments({ + owner: repo.owner, + repo: repo.repo, + pull_number: target, + page, + per_page: 100 + })); + comments.push(...data); + page++; + } while (data.length >= 100); + return comments; + } + catch (e) { + console.warn(`Failed to list review comments: ${e}`); + return comments; + } } - const body = `${COMMENT_GREETING} + async post_comment(message, tag, mode) { + let target; + if (context.payload.pull_request) { + target = context.payload.pull_request.number; + } + else if (context.payload.issue) { + target = context.payload.issue.number; + } + else { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: context.payload.pull_request and context.payload.issue are both null`); + return; + } + if (!tag) { + tag = COMMENT_TAG; + } + const body = `${COMMENT_GREETING} ${message} ${tag}`; - if (mode === 'create') { - await create(body, tag, target); - } - else if (mode === 'replace') { - await replace(body, tag, target); - } - else if (mode === 'append') { - await append(body, tag, target); - } - else if (mode === 'prepend') { - await prepend(body, tag, target); - } - else { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Unknown mode: ${mode}, use "replace" instead`); - await replace(body, tag, target); - } -}; -const create = async (body, tag, target) => { - try { - await octokit.issues.createComment({ - owner: repo.owner, - repo: repo.repo, - issue_number: target, - body - }); - } - catch (e) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to create comment: ${e}`); - } -}; -const replace = async (body, tag, target) => { - try { - const cmt = await find_comment_with_tag(tag, target); - if (cmt) { - await octokit.issues.updateComment({ - owner: repo.owner, - repo: repo.repo, - comment_id: cmt.id, - body - }); + if (mode === 'create') { + await this.create(body, target); } - else { - await create(body, tag, target); + else if (mode === 'replace') { + await this.replace(body, tag, target); } - } - catch (e) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to replace comment: ${e}`); - } -}; -const append = async (body, tag, target) => { - try { - const cmt = await find_comment_with_tag(tag, target); - if (cmt) { - await octokit.issues.updateComment({ - owner: repo.owner, - repo: repo.repo, - comment_id: cmt.id, - body: `${cmt.body} ${body}` - }); + else if (mode === 'append') { + await this.append(body, tag, target); + } + else if (mode === 'prepend') { + await this.prepend(body, tag, target); } else { - await create(body, tag, target); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Unknown mode: ${mode}, use "replace" instead`); + await this.replace(body, tag, target); } } - catch (e) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to append comment: ${e}`); - } -}; -const prepend = async (body, tag, target) => { - try { - const cmt = await find_comment_with_tag(tag, target); - if (cmt) { - await octokit.issues.updateComment({ + async create(body, target) { + try { + await octokit.issues.createComment({ owner: repo.owner, repo: repo.repo, - comment_id: cmt.id, - body: `${body} ${cmt.body}` + issue_number: target, + body }); } - else { - await create(body, tag, target); + catch (e) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to create comment: ${e}`); } } - catch (e) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to prepend comment: ${e}`); + async replace(body, tag, target) { + try { + const cmt = await this.find_comment_with_tag(tag, target); + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body + }); + } + else { + await this.create(body, target); + } + } + catch (e) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to replace comment: ${e}`); + } } -}; -const find_comment_with_tag = async (tag, target) => { - try { - const comments = await list_comments(target); - for (const cmt of comments) { - if (cmt.body && cmt.body.includes(tag)) { - return cmt; + async append(body, tag, target) { + try { + const cmt = await this.find_comment_with_tag(tag, target); + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body: `${cmt.body} ${body}` + }); } + else { + await this.create(body, target); + } + } + catch (e) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to append comment: ${e}`); } - return null; } - catch (e) { - _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to find comment with tag: ${e}`); - return null; + async prepend(body, tag, target) { + try { + const cmt = await this.find_comment_with_tag(tag, target); + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body: `${body} ${cmt.body}` + }); + } + else { + await this.create(body, target); + } + } + catch (e) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to prepend comment: ${e}`); + } } -}; -const list_comments = async (target, page = 1) => { - const comments = []; - try { - let data; - do { - ; - ({ data } = await octokit.issues.listComments({ - owner: repo.owner, - repo: repo.repo, - issue_number: target, - page, - per_page: 100 - })); - comments.push(...data); - page++; - } while (data.length >= 100); - return comments; + async find_comment_with_tag(tag, target) { + try { + const comments = await this.list_comments(target); + for (const cmt of comments) { + if (cmt.body && cmt.body.includes(tag)) { + return cmt; + } + } + return null; + } + catch (e) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to find comment with tag: ${e}`); + return null; + } } - catch (e) { - console.warn(`Failed to list comments: ${e}`); - return comments; + async list_comments(target, page = 1) { + const comments = []; + try { + let data; + do { + ; + ({ data } = await octokit.issues.listComments({ + owner: repo.owner, + repo: repo.repo, + issue_number: target, + page, + per_page: 100 + })); + comments.push(...data); + page++; + } while (data.length >= 100); + return comments; + } + catch (e) { + console.warn(`Failed to list comments: ${e}`); + return comments; + } } -}; +} /***/ }), @@ -27385,7 +27386,7 @@ __nccwpck_require__.a(__webpack_module__, async (__webpack_handle_async_dependen /* harmony import */ var _bot_js__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(5357); /* harmony import */ var _options_js__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(744); /* harmony import */ var _review_comment_js__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(3435); -/* harmony import */ var _review_js__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(899); +/* harmony import */ var _review_js__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(4231); @@ -27393,7 +27394,7 @@ __nccwpck_require__.a(__webpack_module__, async (__webpack_handle_async_dependen async function run() { const options = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Options */ .Ei(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('debug'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('max_files'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('review_comment_lgtm'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getMultilineInput('path_filters'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('system_message'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('temperature')); - const prompts = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Prompts */ .jc(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch_begin'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_release_notes')); + const prompts = new _options_js__WEBPACK_IMPORTED_MODULE_2__/* .Prompts */ .jc(_actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch_begin'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('review_patch'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('summarize_release_notes'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('comment_beginning'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('comment_file'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('comment_file_diff'), _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('comment')); // initialize openai bot let bot = null; try { @@ -27410,7 +27411,7 @@ async function run() { await (0,_review_js__WEBPACK_IMPORTED_MODULE_4__/* .codeReview */ .z)(bot, options, prompts); } else if (process.env.GITHUB_EVENT_NAME === 'pull_request_review_comment') { - await (0,_review_comment_js__WEBPACK_IMPORTED_MODULE_3__/* .handleReviewComment */ .V)(bot); + await (0,_review_comment_js__WEBPACK_IMPORTED_MODULE_3__/* .handleReviewComment */ .V)(bot, prompts); } else { _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning('Skipped: this action only works on push event'); @@ -28950,7 +28951,11 @@ class Prompts { summarize_file_diff; summarize; summarize_release_notes; - constructor(review_beginning = '', review_file = '', review_file_diff = '', review_patch_begin = '', review_patch = '', summarize_beginning = '', summarize_file_diff = '', summarize = '', summarize_release_notes = '') { + comment_beginning; + comment_file; + comment_file_diff; + comment; + constructor(review_beginning = '', review_file = '', review_file_diff = '', review_patch_begin = '', review_patch = '', summarize_beginning = '', summarize_file_diff = '', summarize = '', summarize_release_notes = '', comment_beginning = '', comment_file = '', comment_file_diff = '', comment = '') { this.review_beginning = review_beginning; this.review_file = review_file; this.review_file_diff = review_file_diff; @@ -28960,6 +28965,10 @@ class Prompts { this.summarize_file_diff = summarize_file_diff; this.summarize = summarize; this.summarize_release_notes = summarize_release_notes; + this.comment_beginning = comment_beginning; + this.comment_file = comment_file; + this.comment_file_diff = comment_file_diff; + this.comment = comment; } render_review_beginning(inputs) { return inputs.render(this.review_beginning); @@ -28988,6 +28997,18 @@ class Prompts { render_summarize_release_notes(inputs) { return inputs.render(this.summarize_release_notes); } + render_comment_beginning(inputs) { + return inputs.render(this.comment_beginning); + } + render_comment_file(inputs) { + return inputs.render(this.comment_file); + } + render_comment_file_diff(inputs) { + return inputs.render(this.comment_file_diff); + } + render_comment(inputs) { + return inputs.render(this.comment); + } } class Inputs { system_message; @@ -28999,7 +29020,9 @@ class Inputs { file_diff; patch; diff; - constructor(system_message = '', title = 'no title provided', description = 'no description provided', summary = 'no summary so far', filename = '', file_content = '', file_diff = '', patch = '', diff = '') { + comment_chain; + comment; + constructor(system_message = '', title = 'no title provided', description = 'no description provided', summary = 'no summary so far', filename = '', file_content = '', file_diff = '', patch = '', diff = '', comment_chain = '', comment = '') { this.system_message = system_message; this.title = title; this.description = description; @@ -29009,6 +29032,8 @@ class Inputs { this.file_diff = file_diff; this.patch = patch; this.diff = diff; + this.comment_chain = comment_chain; + this.comment = comment; } render(content) { if (!content) { @@ -29041,6 +29066,12 @@ class Inputs { if (this.diff) { content = content.replace('$diff', this.diff); } + if (this.comment_chain) { + content = content.replace('$comment_chain', this.comment_chain); + } + if (this.comment) { + content = content.replace('$comment', this.comment); + } return content; } } @@ -29119,8 +29150,12 @@ class PathFilter { /* harmony export */ }); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(2186); /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(5438); -/* harmony import */ var _octokit_action__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(1231); +/* harmony import */ var _octokit_action__WEBPACK_IMPORTED_MODULE_5__ = __nccwpck_require__(1231); /* harmony import */ var _commenter_js__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(4571); +/* harmony import */ var _options_js__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(744); +/* harmony import */ var _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(6153); + + @@ -29128,12 +29163,14 @@ class PathFilter { const token = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('token') ? _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('token') : process.env.GITHUB_TOKEN; -const octokit = new _octokit_action__WEBPACK_IMPORTED_MODULE_3__/* .Octokit */ .v({ auth: `token ${token}` }); +const octokit = new _octokit_action__WEBPACK_IMPORTED_MODULE_5__/* .Octokit */ .v({ auth: `token ${token}` }); const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; const repo = context.repo; const ASK_BOT = '@openai'; -const handleReviewComment = async (bot) => { +const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500; +const handleReviewComment = async (bot, prompts) => { const commenter = new _commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .Commenter */ .Es(); + const inputs = new _options_js__WEBPACK_IMPORTED_MODULE_3__/* .Inputs */ .kq(); if (context.eventName !== 'pull_request_review_comment') { _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: ${context.eventName} is not a pull_request_review_comment event`); return; @@ -29151,6 +29188,10 @@ const handleReviewComment = async (bot) => { _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: ${context.eventName} event is missing pull_request`); return; } + inputs.title = context.payload.pull_request.title; + if (context.payload.pull_request.body) { + inputs.description = context.payload.pull_request.body; + } // check if the comment was created and not edited or deleted if (context.payload.action !== 'created') { _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: ${context.eventName} event is not created`); @@ -29160,31 +29201,81 @@ const handleReviewComment = async (bot) => { if (!comment.body.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) && !comment.body.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD)) { const pull_number = context.payload.pull_request.number; - const diffHunk = comment.diff_hunk; - const { chain, topLevelComment } = await commenter.getConversationChain(pull_number, comment); - _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Conversation chain: ${chain}`); + const diff = comment.diff_hunk; + inputs.comment = `${comment.user.login}: ${comment.body}`; + inputs.diff = diff; + const { chain: comment_chain, topLevelComment } = await commenter.getConversationChain(pull_number, comment); + inputs.comment_chain = comment_chain; // check whether this chain contains replies from the bot - if (chain.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) || - chain.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD) || + if (comment_chain.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_TAG */ .Rs) || + comment_chain.includes(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD) || comment.body.startsWith(ASK_BOT)) { - const prompt = `I would like you to reply to the new comment made on a conversation chain on a code review diff. - -Diff: -\`\`\`diff -${diffHunk} -\`\`\` - -Conversation chain (including the new comment): -\`\`\` -${chain} -\`\`\` - -Please reply to the new comment in the conversation chain without extra prose as that reply will be posted as-is. Make sure to tag the user in your reply. Providing below the new comment again as reference: -\`\`\` -${comment.user.login}: ${comment.body} -\`\`\` -`; - const [reply] = await bot.chat(prompt, {}); + let file_content = ''; + let file_diff = ''; + try { + const contents = await octokit.repos.getContent({ + owner: repo.owner, + repo: repo.repo, + path: comment.path, + ref: context.payload.pull_request.base.sha + }); + if (contents.data) { + if (!Array.isArray(contents.data)) { + if (contents.data.type === 'file' && contents.data.content) { + file_content = Buffer.from(contents.data.content, 'base64').toString(); + } + } + } + // get diff for this file by comparing the base and head commits + const diffAll = await octokit.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha + }); + if (diffAll.data) { + const files = diffAll.data.files; + if (files) { + const file = files.find(f => f.filename === comment.path); + if (file && file.patch) { + file_diff = file.patch; + } + } + } + } + catch (error) { + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to get file contents: ${error}, skipping.`); + } + // get summary of the PR + const summary = await commenter.find_comment_with_tag(_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, pull_number); + if (summary) { + inputs.summary = summary.body; + } + inputs.filename = comment.path; + inputs.file_content = file_content; + inputs.file_diff = file_diff; + // begin comment generation + const [, comment_begin_ids] = await bot.chat(prompts.render_comment_beginning(inputs), {}); + let next_comment_ids = comment_begin_ids; + if (file_content.length > 0) { + const file_content_tokens = _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__/* .get_token_count */ .u(file_content); + if (file_content_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { + const [file_content_resp, file_content_ids] = await bot.chat(prompts.render_comment_file(inputs), next_comment_ids); + if (file_content_resp) { + next_comment_ids = file_content_ids; + } + } + } + if (file_diff.length > 0) { + const file_diff_tokens = _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__/* .get_token_count */ .u(file_diff); + if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { + const [file_diff_resp, file_diff_ids] = await bot.chat(prompts.render_comment_file_diff(inputs), next_comment_ids); + if (file_diff_resp) { + next_comment_ids = file_diff_ids; + } + } + } + const [reply] = await bot.chat(prompts.render_comment(inputs), next_comment_ids); const message = `${_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_GREETING */ .pK} ${reply} @@ -29228,65 +29319,43 @@ ${_commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .COMMENT_REPLY_TAG */ .aD} /***/ }), -/***/ 899: +/***/ 4231: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => { - -// EXPORTS -__nccwpck_require__.d(__webpack_exports__, { - "z": () => (/* binding */ codeReview) -}); - -// EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js -var core = __nccwpck_require__(2186); -// EXTERNAL MODULE: ./node_modules/@actions/github/lib/github.js -var github = __nccwpck_require__(5438); -// EXTERNAL MODULE: ./node_modules/@octokit/action/dist-node/index.js -var dist_node = __nccwpck_require__(1231); -// EXTERNAL MODULE: ./lib/commenter.js -var lib_commenter = __nccwpck_require__(4571); -// EXTERNAL MODULE: ./lib/options.js + 4 modules -var lib_options = __nccwpck_require__(744); -// EXTERNAL MODULE: ./node_modules/@dqbd/tiktoken/dist/node/_tiktoken.js -var _tiktoken = __nccwpck_require__(4083); -;// CONCATENATED MODULE: ./lib/tokenizer.js - -const tokenizer = (0,_tiktoken.get_encoding)('cl100k_base'); -function encode(input) { - return tokenizer.encode(input); -} -function get_token_count(input) { - input = input.replace(/<\|endoftext\|>/g, ''); - return encode(input).length; -} - -;// CONCATENATED MODULE: ./lib/review.js +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "z": () => (/* binding */ codeReview) +/* harmony export */ }); +/* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(2186); +/* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(5438); +/* harmony import */ var _octokit_action__WEBPACK_IMPORTED_MODULE_5__ = __nccwpck_require__(1231); +/* harmony import */ var _commenter_js__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(4571); +/* harmony import */ var _options_js__WEBPACK_IMPORTED_MODULE_3__ = __nccwpck_require__(744); +/* harmony import */ var _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__ = __nccwpck_require__(6153); -const token = core.getInput('token') - ? core.getInput('token') +const token = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('token') + ? _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('token') : process.env.GITHUB_TOKEN; -const octokit = new dist_node/* Octokit */.v({ auth: `token ${token}` }); -const context = github.context; +const octokit = new _octokit_action__WEBPACK_IMPORTED_MODULE_5__/* .Octokit */ .v({ auth: `token ${token}` }); +const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context; const repo = context.repo; const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500; -const comment_tag = ''; const codeReview = async (bot, options, prompts) => { - const commenter = new lib_commenter/* Commenter */.Es(); + const commenter = new _commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .Commenter */ .Es(); if (context.eventName !== 'pull_request' && context.eventName !== 'pull_request_target') { - core.warning(`Skipped: current event is ${context.eventName}, only support pull_request event`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: current event is ${context.eventName}, only support pull_request event`); return; } if (!context.payload.pull_request) { - core.warning(`Skipped: context.payload.pull_request is null`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: context.payload.pull_request is null`); return; } - const inputs = new lib_options/* Inputs */.kq(); + const inputs = new _options_js__WEBPACK_IMPORTED_MODULE_3__/* .Inputs */ .kq(); inputs.title = context.payload.pull_request.title; if (context.payload.pull_request.body) { inputs.description = commenter.get_description(context.payload.pull_request.body); @@ -29302,15 +29371,15 @@ const codeReview = async (bot, options, prompts) => { }); const { files, commits } = diff.data; if (!files) { - core.warning(`Skipped: diff.data.files is null`); - await commenter.comment(`Skipped: no files to review`, comment_tag, 'replace'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Skipped: diff.data.files is null`); + await commenter.comment(`Skipped: no files to review`, _commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, 'replace'); return; } // skip files if they are filtered out const filtered_files = []; for (const file of files) { if (!options.check_path(file.filename)) { - core.info(`skip for excluded path: ${file.filename}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`skip for excluded path: ${file.filename}`); continue; } else { @@ -29319,8 +29388,8 @@ const codeReview = async (bot, options, prompts) => { } // check if we are exceeding max_files and if max_files is <= 0 (no limit) if (filtered_files.length > options.max_files && options.max_files > 0) { - core.warning("Skipped: too many files to review, can't handle it"); - await commenter.comment(`Skipped: too many files to review, can't handle it`, comment_tag, 'replace'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning("Skipped: too many files to review, can't handle it"); + await commenter.comment(`Skipped: too many files to review, can't handle it`, _commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, 'replace'); return; } // find patches to review @@ -29344,11 +29413,11 @@ const codeReview = async (bot, options, prompts) => { } } catch (error) { - core.warning(`Failed to get file contents: ${error}, skipping.`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to get file contents: ${error}, skipping.`); } let file_diff = ''; if (file.patch) { - core.info(`diff for ${file.filename}: ${file.patch}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`diff for ${file.filename}: ${file.patch}`); file_diff = file.patch; } const patches = []; @@ -29371,12 +29440,12 @@ const codeReview = async (bot, options, prompts) => { inputs.file_content = file_content; inputs.file_diff = file_diff; if (file_diff.length > 0) { - const file_diff_tokens = get_token_count(file_diff); + const file_diff_tokens = _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__/* .get_token_count */ .u(file_diff); if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // summarize diff const [summarize_resp, summarize_diff_ids] = await bot.chat(prompts.render_summarize_file_diff(inputs), next_summarize_ids); if (!summarize_resp) { - core.info('summarize: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('summarize: nothing obtained from openai'); } else { next_summarize_ids = summarize_diff_ids; @@ -29388,17 +29457,17 @@ const codeReview = async (bot, options, prompts) => { // final summary const [summarize_final_response, summarize_final_response_ids] = await bot.chat(prompts.render_summarize(inputs), next_summarize_ids); if (!summarize_final_response) { - core.info('summarize: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('summarize: nothing obtained from openai'); } else { inputs.summary = summarize_final_response; next_summarize_ids = summarize_final_response_ids; - await commenter.comment(`${summarize_final_response}`, comment_tag, 'replace'); + await commenter.comment(`${summarize_final_response}`, _commenter_js__WEBPACK_IMPORTED_MODULE_2__/* .SUMMARIZE_TAG */ .Rp, 'replace'); } // final release notes const [release_notes_response, release_notes_ids] = await bot.chat(prompts.render_summarize_release_notes(inputs), next_summarize_ids); if (!release_notes_response) { - core.info('release notes: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('release notes: nothing obtained from openai'); } else { next_summarize_ids = release_notes_ids; @@ -29416,46 +29485,46 @@ const codeReview = async (bot, options, prompts) => { // reset chat session for each file while reviewing next_review_ids = review_begin_ids; if (file_content.length > 0) { - const file_content_tokens = get_token_count(file_content); + const file_content_tokens = _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__/* .get_token_count */ .u(file_content); if (file_content_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // review file const [resp, review_file_ids] = await bot.chat(prompts.render_review_file(inputs), next_review_ids); if (!resp) { - core.info('review: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('review: nothing obtained from openai'); } else { next_review_ids = review_file_ids; } } else { - core.info(`skip sending content of file: ${inputs.filename} due to token count: ${file_content_tokens}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`skip sending content of file: ${inputs.filename} due to token count: ${file_content_tokens}`); } } if (file_diff.length > 0) { - const file_diff_tokens = get_token_count(file_diff); + const file_diff_tokens = _tokenizer_js__WEBPACK_IMPORTED_MODULE_4__/* .get_token_count */ .u(file_diff); if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { // review diff const [resp, review_diff_ids] = await bot.chat(prompts.render_review_file_diff(inputs), next_review_ids); if (!resp) { - core.info('review: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('review: nothing obtained from openai'); } else { next_review_ids = review_diff_ids; } } else { - core.info(`skip sending diff of file: ${inputs.filename} due to token count: ${file_diff_tokens}`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`skip sending diff of file: ${inputs.filename} due to token count: ${file_diff_tokens}`); } } // review_patch_begin const [, patch_begin_ids] = await bot.chat(prompts.render_review_patch_begin(inputs), next_review_ids); next_review_ids = patch_begin_ids; for (const [line, patch] of patches) { - core.info(`Reviewing ${filename}:${line} with openai ...`); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info(`Reviewing ${filename}:${line} with openai ...`); inputs.patch = patch; const [response, patch_ids] = await bot.chat(prompts.render_review_patch(inputs), next_review_ids); if (!response) { - core.info('review: nothing obtained from openai'); + _actions_core__WEBPACK_IMPORTED_MODULE_0__.info('review: nothing obtained from openai'); continue; } next_review_ids = patch_ids; @@ -29466,7 +29535,7 @@ const codeReview = async (bot, options, prompts) => { await commenter.review_comment(context.payload.pull_request.number, commits[commits.length - 1].sha, filename, line, `${response}`); } catch (e) { - core.warning(`Failed to comment: ${e}, skipping. + _actions_core__WEBPACK_IMPORTED_MODULE_0__.warning(`Failed to comment: ${e}, skipping. backtrace: ${e.stack} filename: ${filename} line: ${line} @@ -29514,6 +29583,27 @@ const patch_comment_line = (patch) => { }; +/***/ }), + +/***/ 6153: +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => { + +/* harmony export */ __nccwpck_require__.d(__webpack_exports__, { +/* harmony export */ "u": () => (/* binding */ get_token_count) +/* harmony export */ }); +/* unused harmony export encode */ +/* harmony import */ var _dqbd_tiktoken__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(4083); + +const tokenizer = (0,_dqbd_tiktoken__WEBPACK_IMPORTED_MODULE_0__.get_encoding)('cl100k_base'); +function encode(input) { + return tokenizer.encode(input); +} +function get_token_count(input) { + input = input.replace(/<\|endoftext\|>/g, ''); + return encode(input).length; +} + + /***/ }), /***/ 3213: diff --git a/src/commenter.ts b/src/commenter.ts index adb836ca..5ec7053c 100644 --- a/src/commenter.ts +++ b/src/commenter.ts @@ -17,6 +17,9 @@ export const COMMENT_TAG = export const COMMENT_REPLY_TAG = '' +export const SUMMARIZE_TAG = + '' + export const DESCRIPTION_TAG = '' export const DESCRIPTION_TAG_END = @@ -27,7 +30,7 @@ export class Commenter { * @param mode Can be "create", "replace", "append" and "prepend". Default is "replace". */ async comment(message: string, tag: string, mode: string) { - await comment(message, tag, mode) + await this.post_comment(message, tag, mode) } get_description(description: string) { @@ -63,10 +66,9 @@ export class Commenter { // if not found, add the tag and the content to the end of the description const tag_index = description.indexOf(DESCRIPTION_TAG) const tag_end_index = description.indexOf(DESCRIPTION_TAG_END) - const comment = `\n\n${DESCRIPTION_TAG}\n${message}\n${DESCRIPTION_TAG_END}` + const comment = `${DESCRIPTION_TAG}\n${message}\n${DESCRIPTION_TAG_END}` if (tag_index === -1 || tag_end_index === -1) { - let new_description = description - new_description += comment + const new_description = `${description}\n${comment}` await octokit.pulls.update({ owner: repo.owner, repo: repo.repo, @@ -108,7 +110,7 @@ ${message} ${tag}` // replace comment made by this action try { - const comments = await list_review_comments(pull_number) + const comments = await this.list_review_comments(pull_number) for (const comment of comments) { if (comment.path === path && comment.position === line) { // look for tag @@ -140,7 +142,7 @@ ${tag}` async getConversationChain(pull_number: number, comment: any) { try { - const reviewComments = await list_review_comments(pull_number) + const reviewComments = await this.list_review_comments(pull_number) const topLevelComment = await this.getTopLevelComment( reviewComments, comment @@ -148,7 +150,7 @@ ${tag}` const conversationChain = reviewComments .filter((cmt: any) => cmt.in_reply_to_id === topLevelComment.id) - .map((cmt: any) => `${cmt.user.login}-(${cmt.id}): ${cmt.body}`) + .map((cmt: any) => `${cmt.user.login}: ${cmt.body}`) conversationChain.unshift( `${topLevelComment.user.login}: ${topLevelComment.body}` @@ -184,171 +186,171 @@ ${tag}` return topLevelComment } -} -const list_review_comments = async (target: number, page: number = 1) => { - const comments: any[] = [] - try { - let data - do { - ;({data} = await octokit.pulls.listReviewComments({ - owner: repo.owner, - repo: repo.repo, - pull_number: target, - page, - per_page: 100 - })) - comments.push(...data) - page++ - } while (data.length >= 100) - - return comments - } catch (e: any) { - console.warn(`Failed to list review comments: ${e}`) - return comments + async list_review_comments(target: number, page: number = 1) { + const comments: any[] = [] + try { + let data + do { + ;({data} = await octokit.pulls.listReviewComments({ + owner: repo.owner, + repo: repo.repo, + pull_number: target, + page, + per_page: 100 + })) + comments.push(...data) + page++ + } while (data.length >= 100) + + return comments + } catch (e: any) { + console.warn(`Failed to list review comments: ${e}`) + return comments + } } -} -const comment = async (message: string, tag: string, mode: string) => { - let target: number - if (context.payload.pull_request) { - target = context.payload.pull_request.number - } else if (context.payload.issue) { - target = context.payload.issue.number - } else { - core.warning( - `Skipped: context.payload.pull_request and context.payload.issue are both null` - ) - return - } + async post_comment(message: string, tag: string, mode: string) { + let target: number + if (context.payload.pull_request) { + target = context.payload.pull_request.number + } else if (context.payload.issue) { + target = context.payload.issue.number + } else { + core.warning( + `Skipped: context.payload.pull_request and context.payload.issue are both null` + ) + return + } - if (!tag) { - tag = COMMENT_TAG - } + if (!tag) { + tag = COMMENT_TAG + } - const body = `${COMMENT_GREETING} + const body = `${COMMENT_GREETING} ${message} ${tag}` - if (mode === 'create') { - await create(body, tag, target) - } else if (mode === 'replace') { - await replace(body, tag, target) - } else if (mode === 'append') { - await append(body, tag, target) - } else if (mode === 'prepend') { - await prepend(body, tag, target) - } else { - core.warning(`Unknown mode: ${mode}, use "replace" instead`) - await replace(body, tag, target) - } -} - -const create = async (body: string, tag: string, target: number) => { - try { - await octokit.issues.createComment({ - owner: repo.owner, - repo: repo.repo, - issue_number: target, - body - }) - } catch (e: any) { - core.warning(`Failed to create comment: ${e}`) + if (mode === 'create') { + await this.create(body, target) + } else if (mode === 'replace') { + await this.replace(body, tag, target) + } else if (mode === 'append') { + await this.append(body, tag, target) + } else if (mode === 'prepend') { + await this.prepend(body, tag, target) + } else { + core.warning(`Unknown mode: ${mode}, use "replace" instead`) + await this.replace(body, tag, target) + } } -} -const replace = async (body: string, tag: string, target: number) => { - try { - const cmt = await find_comment_with_tag(tag, target) - if (cmt) { - await octokit.issues.updateComment({ + async create(body: string, target: number) { + try { + await octokit.issues.createComment({ owner: repo.owner, repo: repo.repo, - comment_id: cmt.id, + issue_number: target, body }) - } else { - await create(body, tag, target) + } catch (e: any) { + core.warning(`Failed to create comment: ${e}`) } - } catch (e: any) { - core.warning(`Failed to replace comment: ${e}`) } -} -const append = async (body: string, tag: string, target: number) => { - try { - const cmt = await find_comment_with_tag(tag, target) - if (cmt) { - await octokit.issues.updateComment({ - owner: repo.owner, - repo: repo.repo, - comment_id: cmt.id, - body: `${cmt.body} ${body}` - }) - } else { - await create(body, tag, target) + async replace(body: string, tag: string, target: number) { + try { + const cmt = await this.find_comment_with_tag(tag, target) + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body + }) + } else { + await this.create(body, target) + } + } catch (e: any) { + core.warning(`Failed to replace comment: ${e}`) } - } catch (e: any) { - core.warning(`Failed to append comment: ${e}`) } -} -const prepend = async (body: string, tag: string, target: number) => { - try { - const cmt = await find_comment_with_tag(tag, target) - if (cmt) { - await octokit.issues.updateComment({ - owner: repo.owner, - repo: repo.repo, - comment_id: cmt.id, - body: `${body} ${cmt.body}` - }) - } else { - await create(body, tag, target) + async append(body: string, tag: string, target: number) { + try { + const cmt = await this.find_comment_with_tag(tag, target) + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body: `${cmt.body} ${body}` + }) + } else { + await this.create(body, target) + } + } catch (e: any) { + core.warning(`Failed to append comment: ${e}`) } - } catch (e: any) { - core.warning(`Failed to prepend comment: ${e}`) } -} -const find_comment_with_tag = async (tag: string, target: number) => { - try { - const comments = await list_comments(target) - for (const cmt of comments) { - if (cmt.body && cmt.body.includes(tag)) { - return cmt + async prepend(body: string, tag: string, target: number) { + try { + const cmt = await this.find_comment_with_tag(tag, target) + if (cmt) { + await octokit.issues.updateComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: cmt.id, + body: `${body} ${cmt.body}` + }) + } else { + await this.create(body, target) } + } catch (e: any) { + core.warning(`Failed to prepend comment: ${e}`) } + } + + async find_comment_with_tag(tag: string, target: number) { + try { + const comments = await this.list_comments(target) + for (const cmt of comments) { + if (cmt.body && cmt.body.includes(tag)) { + return cmt + } + } - return null - } catch (e: any) { - core.warning(`Failed to find comment with tag: ${e}`) - return null + return null + } catch (e: any) { + core.warning(`Failed to find comment with tag: ${e}`) + return null + } } -} -const list_comments = async (target: number, page: number = 1) => { - const comments: any[] = [] - try { - let data - do { - ;({data} = await octokit.issues.listComments({ - owner: repo.owner, - repo: repo.repo, - issue_number: target, - page, - per_page: 100 - })) - - comments.push(...data) - page++ - } while (data.length >= 100) - - return comments - } catch (e: any) { - console.warn(`Failed to list comments: ${e}`) - return comments + async list_comments(target: number, page: number = 1) { + const comments: any[] = [] + try { + let data + do { + ;({data} = await octokit.issues.listComments({ + owner: repo.owner, + repo: repo.repo, + issue_number: target, + page, + per_page: 100 + })) + + comments.push(...data) + page++ + } while (data.length >= 100) + + return comments + } catch (e: any) { + console.warn(`Failed to list comments: ${e}`) + return comments + } } } diff --git a/src/main.ts b/src/main.ts index 6dadd60d..8823ac48 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,7 +22,11 @@ async function run(): Promise { core.getInput('summarize_beginning'), core.getInput('summarize_file_diff'), core.getInput('summarize'), - core.getInput('summarize_release_notes') + core.getInput('summarize_release_notes'), + core.getInput('comment_beginning'), + core.getInput('comment_file'), + core.getInput('comment_file_diff'), + core.getInput('comment') ) // initialize openai bot @@ -46,7 +50,7 @@ async function run(): Promise { } else if ( process.env.GITHUB_EVENT_NAME === 'pull_request_review_comment' ) { - await handleReviewComment(bot) + await handleReviewComment(bot, prompts) } else { core.warning('Skipped: this action only works on push event') } diff --git a/src/options.ts b/src/options.ts index eb8429b3..1ad38578 100644 --- a/src/options.ts +++ b/src/options.ts @@ -11,6 +11,10 @@ export class Prompts { summarize_file_diff: string summarize: string summarize_release_notes: string + comment_beginning: string + comment_file: string + comment_file_diff: string + comment: string constructor( review_beginning = '', @@ -21,7 +25,11 @@ export class Prompts { summarize_beginning = '', summarize_file_diff = '', summarize = '', - summarize_release_notes = '' + summarize_release_notes = '', + comment_beginning = '', + comment_file = '', + comment_file_diff = '', + comment = '' ) { this.review_beginning = review_beginning this.review_file = review_file @@ -32,6 +40,10 @@ export class Prompts { this.summarize_file_diff = summarize_file_diff this.summarize = summarize this.summarize_release_notes = summarize_release_notes + this.comment_beginning = comment_beginning + this.comment_file = comment_file + this.comment_file_diff = comment_file_diff + this.comment = comment } render_review_beginning(inputs: Inputs): string { @@ -69,6 +81,18 @@ export class Prompts { render_summarize_release_notes(inputs: Inputs): string { return inputs.render(this.summarize_release_notes) } + render_comment_beginning(inputs: Inputs): string { + return inputs.render(this.comment_beginning) + } + render_comment_file(inputs: Inputs): string { + return inputs.render(this.comment_file) + } + render_comment_file_diff(inputs: Inputs): string { + return inputs.render(this.comment_file_diff) + } + render_comment(inputs: Inputs): string { + return inputs.render(this.comment) + } } export class Inputs { @@ -81,6 +105,8 @@ export class Inputs { file_diff: string patch: string diff: string + comment_chain: string + comment: string constructor( system_message = '', @@ -91,7 +117,9 @@ export class Inputs { file_content = '', file_diff = '', patch = '', - diff = '' + diff = '', + comment_chain = '', + comment = '' ) { this.system_message = system_message this.title = title @@ -102,6 +130,8 @@ export class Inputs { this.file_diff = file_diff this.patch = patch this.diff = diff + this.comment_chain = comment_chain + this.comment = comment } render(content: string): string { @@ -135,6 +165,12 @@ export class Inputs { if (this.diff) { content = content.replace('$diff', this.diff) } + if (this.comment_chain) { + content = content.replace('$comment_chain', this.comment_chain) + } + if (this.comment) { + content = content.replace('$comment', this.comment) + } return content } } diff --git a/src/review-comment.ts b/src/review-comment.ts index 97987550..4937e8d9 100644 --- a/src/review-comment.ts +++ b/src/review-comment.ts @@ -6,8 +6,11 @@ import { Commenter, COMMENT_GREETING, COMMENT_REPLY_TAG, - COMMENT_TAG + COMMENT_TAG, + SUMMARIZE_TAG } from './commenter.js' +import {Inputs, Prompts} from './options.js' +import * as tokenizer from './tokenizer.js' const token = core.getInput('token') ? core.getInput('token') @@ -17,9 +20,11 @@ const octokit = new Octokit({auth: `token ${token}`}) const context = github.context const repo = context.repo const ASK_BOT = '@openai' +const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500 -export const handleReviewComment = async (bot: Bot) => { +export const handleReviewComment = async (bot: Bot, prompts: Prompts) => { const commenter: Commenter = new Commenter() + const inputs: Inputs = new Inputs() if (context.eventName !== 'pull_request_review_comment') { core.warning( @@ -42,6 +47,10 @@ export const handleReviewComment = async (bot: Bot) => { core.warning(`Skipped: ${context.eventName} event is missing pull_request`) return } + inputs.title = context.payload.pull_request.title + if (context.payload.pull_request.body) { + inputs.description = context.payload.pull_request.body + } // check if the comment was created and not edited or deleted if (context.payload.action !== 'created') { @@ -55,38 +64,110 @@ export const handleReviewComment = async (bot: Bot) => { !comment.body.includes(COMMENT_REPLY_TAG) ) { const pull_number = context.payload.pull_request.number - const diffHunk = comment.diff_hunk + const diff = comment.diff_hunk + + inputs.comment = `${comment.user.login}: ${comment.body}` + inputs.diff = diff + + const {chain: comment_chain, topLevelComment} = + await commenter.getConversationChain(pull_number, comment) + inputs.comment_chain = comment_chain - const {chain, topLevelComment} = await commenter.getConversationChain( - pull_number, - comment - ) - core.info(`Conversation chain: ${chain}`) // check whether this chain contains replies from the bot if ( - chain.includes(COMMENT_TAG) || - chain.includes(COMMENT_REPLY_TAG) || + comment_chain.includes(COMMENT_TAG) || + comment_chain.includes(COMMENT_REPLY_TAG) || comment.body.startsWith(ASK_BOT) ) { - const prompt = `I would like you to reply to the new comment made on a conversation chain on a code review diff. - -Diff: -\`\`\`diff -${diffHunk} -\`\`\` - -Conversation chain (including the new comment): -\`\`\` -${chain} -\`\`\` - -Please reply to the new comment in the conversation chain without extra prose as that reply will be posted as-is. Make sure to tag the user in your reply. Providing below the new comment again as reference: -\`\`\` -${comment.user.login}: ${comment.body} -\`\`\` -` + let file_content = '' + let file_diff = '' + try { + const contents = await octokit.repos.getContent({ + owner: repo.owner, + repo: repo.repo, + path: comment.path, + ref: context.payload.pull_request.base.sha + }) + if (contents.data) { + if (!Array.isArray(contents.data)) { + if (contents.data.type === 'file' && contents.data.content) { + file_content = Buffer.from( + contents.data.content, + 'base64' + ).toString() + } + } + } + // get diff for this file by comparing the base and head commits + const diffAll = await octokit.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: context.payload.pull_request.base.sha, + head: context.payload.pull_request.head.sha + }) + if (diffAll.data) { + const files = diffAll.data.files + if (files) { + const file = files.find(f => f.filename === comment.path) + if (file && file.patch) { + file_diff = file.patch + } + } + } + } catch (error) { + core.warning(`Failed to get file contents: ${error}, skipping.`) + } + + // get summary of the PR + const summary = await commenter.find_comment_with_tag( + SUMMARIZE_TAG, + pull_number + ) + if (summary) { + inputs.summary = summary.body + } + + inputs.filename = comment.path + inputs.file_content = file_content + inputs.file_diff = file_diff + + // begin comment generation + const [, comment_begin_ids] = await bot.chat( + prompts.render_comment_beginning(inputs), + {} + ) + let next_comment_ids = comment_begin_ids + if (file_content.length > 0) { + const file_content_tokens = tokenizer.get_token_count(file_content) + if (file_content_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { + const [file_content_resp, file_content_ids] = await bot.chat( + prompts.render_comment_file(inputs), + next_comment_ids + ) + if (file_content_resp) { + next_comment_ids = file_content_ids + } + } + } + + if (file_diff.length > 0) { + const file_diff_tokens = tokenizer.get_token_count(file_diff) + if (file_diff_tokens < MAX_TOKENS_FOR_EXTRA_CONTENT) { + const [file_diff_resp, file_diff_ids] = await bot.chat( + prompts.render_comment_file_diff(inputs), + next_comment_ids + ) + if (file_diff_resp) { + next_comment_ids = file_diff_ids + } + } + } + + const [reply] = await bot.chat( + prompts.render_comment(inputs), + next_comment_ids + ) - const [reply] = await bot.chat(prompt, {}) const message = `${COMMENT_GREETING} ${reply} diff --git a/src/review.ts b/src/review.ts index 9ae25b72..e67c764c 100644 --- a/src/review.ts +++ b/src/review.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core' import * as github from '@actions/github' import {Octokit} from '@octokit/action' import {Bot} from './bot.js' -import {Commenter} from './commenter.js' +import {Commenter, SUMMARIZE_TAG} from './commenter.js' import {Inputs, Options, Prompts} from './options.js' import * as tokenizer from './tokenizer.js' @@ -14,8 +14,6 @@ const context = github.context const repo = context.repo const MAX_TOKENS_FOR_EXTRA_CONTENT = 2500 -const comment_tag = - '' export const codeReview = async ( bot: Bot, @@ -60,7 +58,7 @@ export const codeReview = async ( core.warning(`Skipped: diff.data.files is null`) await commenter.comment( `Skipped: no files to review`, - comment_tag, + SUMMARIZE_TAG, 'replace' ) return @@ -82,7 +80,7 @@ export const codeReview = async ( core.warning("Skipped: too many files to review, can't handle it") await commenter.comment( `Skipped: too many files to review, can't handle it`, - comment_tag, + SUMMARIZE_TAG, 'replace' ) return @@ -171,7 +169,7 @@ export const codeReview = async ( next_summarize_ids = summarize_final_response_ids await commenter.comment( `${summarize_final_response}`, - comment_tag, + SUMMARIZE_TAG, 'replace' ) }