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

PR: evaluate code review comments with OpenAI #79

Merged
merged 30 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
52bd951
feat: evaluate code reviews comments with openai
EresDev Aug 8, 2024
b6dc127
fix: give relevance 1 to PR comment with no-code
EresDev Aug 9, 2024
9786ada
fix: remove PR fixed relevance to use openai
EresDev Aug 9, 2024
a03d14a
test: change issue to test PR review comments
EresDev Aug 9, 2024
4af0362
fix: give default relevance 1 to pull spec in test
EresDev Aug 12, 2024
db161e9
Merge branch 'development' of https://github.com/ubiquibot/conversati…
EresDev Aug 13, 2024
7d6ebbc
refactor: add return type to method
EresDev Aug 13, 2024
b223454
refactor: remove redundant type
EresDev Aug 13, 2024
3d32bc9
refactor: move types to correct file
EresDev Aug 13, 2024
3d114d1
fix: add missing http mocks for target issue 5 & its pr 12
EresDev Aug 16, 2024
ee199db
refactor: improve how diff_hunk property is added
EresDev Aug 16, 2024
251f0ce
fix: use correct prompt for review comments
EresDev Aug 16, 2024
32d98ff
refactor: improve combining relevances for different types
EresDev Aug 16, 2024
ce2d0b2
refactor: remove redundant mocks
EresDev Aug 16, 2024
7090b65
fix: increase openai max token limit 3x
EresDev Aug 16, 2024
f56d997
fix: get code review evaulation without issue specs
EresDev Aug 16, 2024
99089b2
refactor: rename diff_hunk to diffHunk
EresDev Aug 19, 2024
df15cb3
refactor: rename missing diff_hunk to diffHunk
EresDev Aug 19, 2024
2e45590
Revert "fix: get code review evaulation without issue specs"
EresDev Aug 21, 2024
e379989
fix: send all PR comments to openai for evaluation
EresDev Aug 24, 2024
4549c80
fix: update ai prompt to cover all PR comment types
EresDev Aug 24, 2024
464703d
Merge branch 'development' of https://github.com/ubiquibot/conversati…
EresDev Aug 24, 2024
041edf9
test: update expected output with openai relevance
EresDev Aug 24, 2024
ee86b77
test: update expected output using openai relevance
EresDev Aug 24, 2024
ee9dd9e
refactor: rename reviewComments to prComments
EresDev Aug 24, 2024
64a00fd
Merge branch 'development' of https://github.com/ubiquibot/conversati…
EresDev Sep 9, 2024
cead79b
Merge branch 'development' of https://github.com/ubiquibot/conversati…
EresDev Sep 10, 2024
bbb22e6
fix: apply fixes to broken merge
EresDev Sep 10, 2024
2da6b86
fix: use camel casing for property name
EresDev Sep 17, 2024
47e7df8
Merge branch 'development' of https://github.com/ubiquibot/conversati…
EresDev Sep 21, 2024
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
16 changes: 0 additions & 16 deletions src/configuration/content-evaluator-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,6 @@ export const contentEvaluatorConfigurationType = Type.Object({
role: ["ISSUE_SPECIFICATION"],
relevance: 1,
},
{
EresDev marked this conversation as resolved.
Show resolved Hide resolved
role: ["PULL_AUTHOR"],
relevance: 1,
},
{
role: ["PULL_ASSIGNEE"],
relevance: 1,
},
{
role: ["PULL_COLLABORATOR"],
relevance: 1,
},
{
role: ["PULL_CONTRIBUTOR"],
relevance: 1,
},
],
}
),
Expand Down
99 changes: 74 additions & 25 deletions src/parser/content-evaluator-module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Value } from "@sinclair/typebox/value";
import Decimal from "decimal.js";
import { encodingForModel, Tiktoken } from "js-tiktoken";
import OpenAI from "openai";
import { commentEnum, CommentType } from "../configuration/comment-types";
import configuration from "../configuration/config-reader";
import { OPENAI_API_KEY } from "../configuration/constants";
import {
ContentEvaluatorConfiguration,
contentEvaluatorConfigurationType,
} from "../configuration/content-evaluator-config";
import logger from "../helpers/logger";
import { IssueActivity } from "../issue-activity";
import openAiRelevanceResponseSchema, { RelevancesByOpenAi } from "../types/openai-type";
import { GithubCommentScore, Module, Result } from "./processor";
import { Value } from "@sinclair/typebox/value";
import { commentEnum, CommentKind, CommentType } from "../configuration/comment-types";
import logger from "../helpers/logger";
import {
openAiRelevanceResponseSchema,
CommentToEvaluate,
Relevances,
PrCommentToEvaluate,
} from "../types/content-evaluator-module-type";

/**
* Evaluates and rates comments.
Expand Down Expand Up @@ -76,22 +81,9 @@ export class ContentEvaluatorModule implements Module {

async _processComment(comments: Readonly<GithubCommentScore>[], specificationBody: string) {
const commentsWithScore: GithubCommentScore[] = [...comments];
const { commentsToEvaluate, prCommentsToEvaluate } = this._splitCommentsByPrompt(commentsWithScore);

// exclude comments that have fixed relevance multiplier. e.g. review comments = 1
const commentsToEvaluate: { id: number; comment: string }[] = [];
for (let i = 0; i < commentsWithScore.length; i++) {
const currentComment = commentsWithScore[i];
if (!this._fixedRelevances[currentComment.type]) {
commentsToEvaluate.push({
id: currentComment.id,
comment: currentComment.content,
});
}
}

const relevancesByAI = commentsToEvaluate.length
? await this._evaluateComments(specificationBody, commentsToEvaluate)
: {};
const relevancesByAI = await this._evaluateComments(specificationBody, commentsToEvaluate, prCommentsToEvaluate);

if (Object.keys(relevancesByAI).length !== commentsToEvaluate.length) {
console.error("Relevance / Comment length mismatch! \nWill use 1 as relevance for missing comments.");
Expand Down Expand Up @@ -133,14 +125,60 @@ export class ContentEvaluatorModule implements Module {
}, {});
}

_splitCommentsByPrompt(commentsWithScore: Readonly<GithubCommentScore>[]): {
commentsToEvaluate: CommentToEvaluate[];
prCommentsToEvaluate: PrCommentToEvaluate[];
} {
const commentsToEvaluate: CommentToEvaluate[] = [];
const prCommentsToEvaluate: PrCommentToEvaluate[] = [];
for (let i = 0; i < commentsWithScore.length; i++) {
const currentComment = commentsWithScore[i];
if (!this._fixedRelevances[currentComment.type]) {
if (currentComment.type & CommentKind.PULL) {
prCommentsToEvaluate.push({
id: currentComment.id,
comment: currentComment.content,
diffHunk: currentComment?.diffHunk,
});
} else {
commentsToEvaluate.push({
id: currentComment.id,
comment: currentComment.content,
});
}
}
}
return { commentsToEvaluate, prCommentsToEvaluate };
}

async _evaluateComments(
specification: string,
comments: { id: number; comment: string }[]
): Promise<RelevancesByOpenAi> {
const prompt = this._generatePrompt(specification, comments);
const dummyResponse = JSON.stringify(this._generateDummyResponse(comments), null, 2);
const maxTokens = this._calculateMaxTokens(dummyResponse);
comments: CommentToEvaluate[],
prComments: PrCommentToEvaluate[]
): Promise<Relevances> {
let commentRelevances: Relevances = {};
let prCommentRelevances: Relevances = {};

if (comments.length) {
const dummyResponse = JSON.stringify(this._generateDummyResponse(comments), null, 2);
const maxTokens = this._calculateMaxTokens(dummyResponse);

const promptForComments = this._generatePromptForComments(specification, comments);
commentRelevances = await this._submitPrompt(promptForComments, maxTokens);
}

if (prComments.length) {
const dummyResponse = JSON.stringify(this._generateDummyResponse(prComments), null, 2);
const maxTokens = this._calculateMaxTokens(dummyResponse);

const promptForPrComments = this._generatePromptForPrComments(specification, prComments);
prCommentRelevances = await this._submitPrompt(promptForPrComments, maxTokens);
}

return { ...commentRelevances, ...prCommentRelevances };
}

async _submitPrompt(prompt: string, maxTokens: number): Promise<Relevances> {
const response: OpenAI.Chat.ChatCompletion = await this._openAi.chat.completions.create({
model: this._configuration?.openAi.model || "gpt-4o-2024-08-06",
response_format: { type: "json_object" },
Expand Down Expand Up @@ -172,7 +210,7 @@ export class ContentEvaluatorModule implements Module {
}
}

_generatePrompt(issue: string, comments: { id: number; comment: string }[]) {
_generatePromptForComments(issue: string, comments: CommentToEvaluate[]) {
if (!issue?.length) {
throw new Error("Issue specification comment is missing or empty");
}
Expand All @@ -182,4 +220,15 @@ export class ContentEvaluatorModule implements Module {
comments.length
}.`;
}

_generatePromptForPrComments(issue: string, comments: PrCommentToEvaluate[]) {
if (!issue?.length) {
throw new Error("Issue specification comment is missing or empty");
}
return `I need to evaluate the value of a GitHub contributor's comments in a pull request. Some of these comments are code review comments, and some are general suggestions or a part of the discussion. I'm interested in how much each comment helps to solve the GitHub issue and improve code quality. Please provide a float between 0 and 1 to represent the value of each comment. A score of 1 indicates that the comment is very valuable and significantly improves the submitted solution and code quality, whereas a score of 0 indicates a negative or zero impact. A stringified JSON is given below that contains the specification of the GitHub issue, and comments by different contributors. The property "diffHunk" presents the chunk of code being addressed for a possible change in a code review comment. \n\n\`\`\`\n${JSON.stringify(
{ specification: issue, comments: comments }
EresDev marked this conversation as resolved.
Show resolved Hide resolved
)}\n\`\`\`\n\n\nTo what degree are each of the comments valuable? Please reply with ONLY a JSON where each key is the comment ID given in JSON above, and the value is a float number between 0 and 1 corresponding to the comment. The float number should represent the value of the comment for improving the issue solution and code quality. The total number of properties in your JSON response should equal exactly ${
comments.length
EresDev marked this conversation as resolved.
Show resolved Hide resolved
}.`;
}
}
5 changes: 5 additions & 0 deletions src/parser/data-purge-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import configuration from "../configuration/config-reader";
import { DataPurgeConfiguration, dataPurgeConfigurationType } from "../configuration/data-purge-config";
import { IssueActivity } from "../issue-activity";
import { Module, Result } from "./processor";
import { GitHubPullRequestReviewComment } from "../github-types";

/**
* Removes the data in the comments that we do not want to be processed.
Expand All @@ -29,6 +30,9 @@ export class DataPurgeModule implements Module {
// Keep only one new line needed by markdown-it package to convert to html
.replace(/\n\s*\n/g, "\n")
.trim();

const reviewComment = comment as GitHubPullRequestReviewComment;

if (newContent.length) {
result[comment.user.login].comments = [
...(result[comment.user.login].comments ?? []),
Expand All @@ -37,6 +41,7 @@ export class DataPurgeModule implements Module {
content: newContent,
url: comment.html_url,
type: comment.type,
diffHunk: reviewComment?.pull_request_review_id ? reviewComment?.diff_hunk : undefined,
},
];
}
Expand Down
1 change: 1 addition & 0 deletions src/parser/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface GithubCommentScore {
content: string;
url: string;
type: CommentKind | CommentAssociation;
diffHunk?: string;
score?: {
formatting?: {
content: Record<string, { symbols: { [p: string]: { count: number; multiplier: number } }; score: number }>;
Expand Down
9 changes: 9 additions & 0 deletions src/types/content-evaluator-module-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Type, Static } from "@sinclair/typebox";

export type CommentToEvaluate = { id: number; comment: string };

export type PrCommentToEvaluate = { id: number; comment: string; diffHunk?: string };

export const openAiRelevanceResponseSchema = Type.Record(Type.String(), Type.Number({ minimum: 0, maximum: 1 }));

export type Relevances = Static<typeof openAiRelevanceResponseSchema>;
7 changes: 0 additions & 7 deletions src/types/openai-type.ts

This file was deleted.

33 changes: 33 additions & 0 deletions tests/__mocks__/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { http, HttpResponse } from "msw";
import { db } from "./db";
import issue5Get from "./routes/issue-5-conversation-rewards/issue-5-get.json";
import issue22CommentsGet from "./routes/issue-22-comments-get.json";
import issue5EventsGet from "./routes/issue-5-conversation-rewards/issue-5-events-get.json";
import issue22Get from "./routes/issue-22-get.json";
import issue25CommentsGet from "./routes/issue-25-comments-get.json";
import issue69EventsGet from "./routes/issue-69-events-get.json";
import issue69CommentsGet from "./routes/issue-69-comments-get.json";
import issue69Get from "./routes/issue-69-get.json";
import issueEvents2Get from "./routes/issue-events-2-get.json";
import issueEventsGet from "./routes/issue-events-get.json";
import issue5CommentsGet from "./routes/issue-5-conversation-rewards/issue-5-comments-get.json";
import issue12CommentsGet from "./routes/pull-12-conversation-rewards/issue-12-comments-get.json";
import pull12Get from "./routes/pull-12-conversation-rewards/pull-12-get.json";
import pull12ReviewsGet from "./routes/pull-12-conversation-rewards/pull-12-reviews-get.json";
import pull12CommentsGet from "./routes/pull-12-conversation-rewards/pull-12-comments-get.json";
import issueTimelineGet from "./routes/issue-timeline-get.json";
import issue69TimelineGet from "./routes/issue-69-timeline-get.json";
import issue70CommentsGet from "./routes/issue-70-comments-get.json";
import pullsCommentsGet from "./routes/pulls-comments-get.json";
import issue5TimelineGet from "./routes/issue-5-conversation-rewards/issue-5-timeline-get.json";
import pullsGet from "./routes/pulls-get.json";
import pulls70Get from "./routes/issue-70-get.json";
import pullsReviewsGet from "./routes/pulls-reviews-get.json";
Expand All @@ -20,6 +28,31 @@ import pullsReviewsGet from "./routes/pulls-reviews-get.json";
* Intercepts the routes and returns a custom payload
*/
export const handlers = [
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/issues/5", () => {
return HttpResponse.json(issue5Get);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/issues/5/events", () => {
return HttpResponse.json(issue5EventsGet);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/issues/5/comments", () => {
return HttpResponse.json(issue5CommentsGet);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/issues/12/comments", () => {
return HttpResponse.json(issue12CommentsGet);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/issues/5/timeline", () => {
return HttpResponse.json(issue5TimelineGet);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/pulls/12", () => {
return HttpResponse.json(pull12Get);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/pulls/12/reviews", () => {
return HttpResponse.json(pull12ReviewsGet);
}),
http.get("https://api.github.com/repos/ubiquibot/conversation-rewards/pulls/12/comments", () => {
return HttpResponse.json(pull12CommentsGet);
}),

http.get("https://api.github.com/repos/ubiquibot/comment-incentives/issues/22", () => {
return HttpResponse.json(issue22Get);
}),
Expand Down
Loading