2025-03-12 22:30:10 +00:00
import { determineValidHunks , fileDiffsToMap , isDiffContentsWellFormed } from "../diff_utils.js"
import { userNickname } from "../auth0-mgmt.js"
import { getInstallationOctokitByOwner , isUserCollaborator } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
2025-03-19 23:19:09 +00:00
import { buildDependentPrTitle , buildPrCommentBody } from "../github/pr-changes-utils.js"
2024-02-21 00:39:58 +00:00
import {
createDependentPullRequest ,
createNewBranchFromDiffContents ,
2025-03-12 22:30:10 +00:00
} from "../github/create-pr-from-diffcontents.js"
import { posthog } from "../analytics.js"
2025-03-24 19:58:12 +00:00
import suggester from "@codeflash-ai/code-suggester"
import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
2024-07-13 01:05:09 +00:00
import { PrismaClient } from "@prisma/client"
2025-03-12 22:30:10 +00:00
import { sendSlackMessage } from "../github/slack_util.js"
2025-01-04 18:57:09 +00:00
import { Request , Response } from "express"
2025-03-12 22:30:10 +00:00
import { AnyOctokit , AuthorizedUserReq , PullRequestDB } from "../types.js"
2024-07-13 01:05:09 +00:00
2025-01-04 18:57:09 +00:00
interface CustomRequest extends Request {
2025-02-21 05:18:55 +00:00
userId? : string
2025-01-04 18:57:09 +00:00
}
2024-07-13 01:05:09 +00:00
const prisma = new PrismaClient ( )
2024-02-21 00:39:58 +00:00
2024-12-19 19:17:18 +00:00
const slackNotificationConfig = {
"Future-House" : [ "aviary" , "paper-qa" ] ,
"langflow-ai" : [ "langflow" ] ,
2025-03-07 01:53:20 +00:00
"albumentations-team" : [ "albumentations" ] ,
2025-03-19 23:19:09 +00:00
"Skyvern-AI" : [ "skyvern" ] ,
2025-04-15 22:25:24 +00:00
roboflow : [ "inference" ] ,
2024-12-19 19:17:18 +00:00
}
2025-03-12 22:30:10 +00:00
export async function suggestPrChanges (
req : AuthorizedUserReq ,
res : Response ,
) : Promise < Response | undefined > {
2024-02-21 00:39:58 +00:00
try {
2024-02-21 23:37:50 +00:00
const {
owner ,
repo ,
pullNumber ,
diffContents ,
prCommentFields ,
existingTests ,
generatedTests ,
2024-11-27 01:01:00 +00:00
coverage_message ,
2024-02-21 23:37:50 +00:00
} = req . body
2025-01-06 22:01:20 +00:00
const userId = req . userId
2024-07-13 01:05:09 +00:00
//traceId is optional to allow for backwards compatibility, can make this required in the future
const traceId = req . body . traceId || ""
console . log ( ` traceId: ${ traceId } ` )
2024-02-21 00:39:58 +00:00
if ( ! repo || ! owner || ! pullNumber || ! isDiffContentsWellFormed ( diffContents ) ) {
return res . status ( 400 ) . send ( "Missing or malformed fields" )
}
const nickname = await userNickname ( userId )
if ( nickname == null ) {
return res . status ( 401 ) . send ( "Unauthorized" ) // Error getting user nickname
}
const installationOctokit = await getInstallationOctokitByOwner ( githubApp , owner , repo )
if ( installationOctokit instanceof Error ) {
return res . status ( 401 ) . send ( installationOctokit . message )
}
const isCollaborator = await isUserCollaborator ( installationOctokit , owner , repo , nickname )
if ( ! isCollaborator ) {
console . log ( ` ${ nickname } is not a collaborator on ${ owner } / ${ repo } ` )
return res . status ( 401 ) . send ( "Unauthorized" ) // User is not a collaborator
}
console . log ( ` ${ nickname } is a collaborator on ${ owner } / ${ repo } ` )
2025-05-14 17:38:36 +00:00
// Check if the owner is roboflow
if ( owner === "roboflow" ) {
console . log ( ` Rejecting request for roboflow repository ` )
return res . status ( 401 ) . send ( "Unauthorized for roboflow repositories" )
}
2024-02-21 00:39:58 +00:00
// Suggest changes
const diffContentsMap : Map < string , FileDiffContent > = fileDiffsToMap ( diffContents )
2025-01-06 22:01:20 +00:00
const { validHunks , invalidHunks } = await determineValidHunks (
2025-03-21 19:38:31 +00:00
installationOctokit . rest as AnyOctokit ,
2024-02-21 00:39:58 +00:00
{ owner , repo } ,
pullNumber ,
100 ,
diffContentsMap ,
)
2025-02-21 05:18:55 +00:00
// a timestamp format like 2024-01-31-12.59.48
const timestamp = new Date ( )
. toISOString ( )
. replace ( /:/g , "." )
. replace ( /\.\d+Z$/ , "" )
// If you change this, please also change the regex in github-app.ts in the pull_request.closed event
const newBranchName = ` codeflash/optimize-pr ${ pullNumber } - ${ timestamp } `
// Get the head branch of the original pull request
const originalPrData = await installationOctokit . rest . pulls . get ( {
owner ,
repo ,
pull_number : pullNumber ,
} )
const baseBranch = originalPrData . data . head . ref
2025-03-26 03:55:12 +00:00
console . log ( ` Attempting to access ref for: ${ owner } / ${ repo } , branch: ${ baseBranch } ` )
2025-02-21 05:18:55 +00:00
const commitMessage =
buildDependentPrTitle (
prCommentFields . function_name ,
prCommentFields . speedup_pct ,
2025-03-11 23:39:10 +00:00
prCommentFields . speedup_x ,
2025-02-21 05:18:55 +00:00
pullNumber ,
baseBranch ,
) + ` \ n ${ prCommentFields . optimization_explanation } `
const branchCreated = await createNewBranchFromDiffContents (
installationOctokit ,
owner ,
repo ,
newBranchName ,
baseBranch ,
diffContentsMap ,
commitMessage ,
)
if ( ! branchCreated ) {
throw new Error ( ` Failed to create branch ${ newBranchName } ` )
}
2025-04-02 01:19:57 +00:00
let hasMultipleHunksInSameFile = false
let hasMultipleFiles = validHunks . size > 1
for ( const [ filePath , hunks ] of validHunks . entries ( ) ) {
if ( hunks . length > 1 ) {
console . log (
` File ${ filePath } has ${ hunks . length } hunks, using dependent PR instead of review comments ` ,
)
hasMultipleHunksInSameFile = true
break
}
}
if ( hasMultipleFiles ) {
console . log (
` Found ${ validHunks . size } files with changes, using dependent PR instead of review comments ` ,
)
}
2025-03-21 01:07:15 +00:00
// Modified condition: Create a dependent PR if there are invalid hunks OR multiple valid hunks
// This addresses both the invalid hunks case and the code suggester library issues
2025-04-02 01:19:57 +00:00
if (
invalidHunks . size > 0 ||
validHunks . size > 1 ||
hasMultipleFiles ||
hasMultipleHunksInSameFile
) {
2024-02-21 00:39:58 +00:00
// we can't suggest all of the hunks for this PR, because some of them are invalid (out of scope for this PR).
// so instead, let's make a new branch, commit the changes,
// and make a new PR from that branch onto the PR's branch
console . log (
2025-03-21 01:07:15 +00:00
` Creating a dependent PR because there are ${ invalidHunks . size > 0 ? "invalid hunks" : "multiple valid hunks" } . ` ,
2024-02-21 00:39:58 +00:00
)
console . log ( ` Making a new dependent PR... ` )
const newPrData = await createDependentPullRequest (
2025-03-12 22:30:10 +00:00
installationOctokit as AnyOctokit ,
2024-02-21 00:39:58 +00:00
owner ,
repo ,
pullNumber ,
newBranchName ,
baseBranch ,
prCommentFields ,
2024-02-21 23:37:50 +00:00
existingTests ,
2024-02-21 00:39:58 +00:00
generatedTests ,
2025-03-12 22:30:10 +00:00
"" ,
2024-02-21 00:39:58 +00:00
)
// Respond with the new PR details
console . log (
` Created new dependent PR # ${ newPrData . data . number } from branch ${ newPrData . data . head . ref } ` ,
)
2024-09-14 03:12:16 +00:00
2025-01-04 18:57:09 +00:00
if ( slackNotificationConfig [ owner as keyof typeof slackNotificationConfig ] ? . includes ( repo ) ) {
2025-01-02 19:12:30 +00:00
await sendSlackMessage (
` new dependent PR created: ${ newPrData . data . html_url } for ${ owner } / ${ repo } ` ,
)
2024-09-14 03:12:16 +00:00
}
2024-11-14 23:58:34 +00:00
2024-02-21 00:39:58 +00:00
posthog . capture ( {
distinctId : userId ,
event : ` cfapi-suggest-pr-changes-success-dependent-pr-created ` ,
properties : {
2024-03-01 01:10:15 +00:00
owner ,
repo ,
2024-02-21 00:39:58 +00:00
newPrNumber : newPrData.data.number ,
newPrBranch : newPrData.data.head.ref ,
2024-06-07 19:26:05 +00:00
PRURL : newPrData.data.html_url ,
2024-02-21 00:39:58 +00:00
} ,
} )
2024-07-13 01:05:09 +00:00
if ( traceId !== "" ) {
let pull_request_db = await prisma . optimization_features . findUnique ( {
where : {
trace_id : traceId ,
} ,
select : {
pull_request : true ,
} ,
} )
2024-07-20 05:15:57 +00:00
if ( pull_request_db ) {
2025-01-06 22:01:20 +00:00
if ( pull_request_db . pull_request === null || pull_request_db . pull_request === undefined ) {
2024-07-20 05:15:57 +00:00
pull_request_db . pull_request = { }
}
2025-03-12 22:30:10 +00:00
; ( pull_request_db as PullRequestDB ) . pull_request . dependent_pr_url =
newPrData . data . html_url
2024-07-13 01:05:09 +00:00
await prisma . optimization_features . update ( {
where : {
trace_id : traceId ,
} ,
2025-01-06 22:01:20 +00:00
data : pull_request_db ,
2024-07-13 01:05:09 +00:00
} )
}
}
2024-02-21 00:39:58 +00:00
res . json ( newPrData . data . number )
} else {
2025-03-21 01:07:15 +00:00
// Only use the code suggester library when there is exactly one valid hunk and no invalid hunks
2025-03-26 03:55:12 +00:00
console . log ( ` Creating unified review for a single valid hunk. ` )
// Include all the information in a single PR comment
2025-01-04 18:57:09 +00:00
const prCommentBody = buildPrCommentBody (
prCommentFields ,
existingTests ,
generatedTests ,
coverage_message ,
2025-02-21 05:18:55 +00:00
newBranchName ,
2025-03-26 03:55:12 +00:00
{ isUnifiedReview : true , includeHeader : false } ,
2025-01-04 18:57:09 +00:00
)
2025-04-02 01:19:57 +00:00
let reviewComments = [ ]
let foundInvalidHunk = false
2024-02-21 00:39:58 +00:00
2025-03-26 03:55:12 +00:00
// Process each valid hunk entry in the Map
for ( const [ filePath , hunks ] of validHunks . entries ( ) ) {
// For each hunk in this file
for ( const hunk of hunks ) {
2025-04-02 01:19:57 +00:00
if ( hunk . oldStart < hunk . oldEnd ) {
const newContent = hunk . newContent . join ( "\n" )
const isLongDiff = newContent . length > 500
let commentBody
if ( isLongDiff ) {
// Create a collapsible section for long diffs
commentBody =
prCommentBody +
"\n\n" +
"<details>\n" +
"<summary>Click to see suggested changes</summary>\n\n" +
"```suggestion\n" +
newContent +
"\n```\n" +
"</details>"
} else {
// For smaller diffs, show them directly
commentBody = prCommentBody + "\n\n" + "```suggestion\n" + newContent + "\n```"
}
reviewComments . push ( {
path : filePath ,
line : hunk.oldEnd ,
start_line : hunk.oldStart ,
side : "RIGHT" ,
body : commentBody , // Format as a suggestion
} )
} else {
console . log (
` Found invalid review range for ${ filePath } : start_line ( ${ hunk . oldStart } ) must be less than line ( ${ hunk . oldEnd } ) with content ${ hunk . newContent . join ( "\n" ) } ` ,
)
foundInvalidHunk = true
break
}
2025-03-26 03:55:12 +00:00
}
2024-02-21 00:39:58 +00:00
2025-04-02 01:19:57 +00:00
if ( foundInvalidHunk ) {
break
}
2024-09-14 03:12:16 +00:00
}
2025-04-02 01:19:57 +00:00
// Add explanation comment
if ( ! foundInvalidHunk && reviewComments . length > 0 ) {
const review = await installationOctokit . rest . pulls . createReview ( {
2024-03-01 01:10:15 +00:00
owner ,
repo ,
2025-04-02 01:19:57 +00:00
pull_number : pullNumber ,
commit_id : originalPrData.data.head.sha ,
event : "COMMENT" ,
comments : reviewComments ,
} )
console . log ( ` Added review comment to PR # ${ pullNumber } : ${ review . data . html_url } ` )
2024-09-14 03:12:16 +00:00
2025-04-02 01:19:57 +00:00
if (
slackNotificationConfig [ owner as keyof typeof slackNotificationConfig ] ? . includes ( repo )
) {
await sendSlackMessage ( ` Suggestions made for ${ review . data . html_url } in ${ owner } / ${ repo } ` )
}
posthog . capture ( {
distinctId : userId ,
event : ` cfapi-suggest-pr-changes-success-suggestions-made ` ,
properties : {
owner ,
repo ,
reviewId : review.data.id ,
PRURL : review.data.html_url ,
2024-07-13 01:05:09 +00:00
} ,
} )
2025-04-02 01:19:57 +00:00
if ( traceId !== "" ) {
let pull_request_db = await prisma . optimization_features . findUnique ( {
2024-07-13 01:05:09 +00:00
where : {
trace_id : traceId ,
} ,
2025-04-02 01:19:57 +00:00
select : {
pull_request : true ,
} ,
2024-07-13 01:05:09 +00:00
} )
2025-04-02 01:19:57 +00:00
if ( pull_request_db ) {
// the trace_id is not in the database then ignore it, because it should already exist by this stage.
if (
pull_request_db . pull_request === null ||
pull_request_db . pull_request === undefined
) {
pull_request_db . pull_request = { }
}
; ( pull_request_db as PullRequestDB ) . pull_request . review_suggestion_pr_url =
review . data . html_url
await prisma . optimization_features . update ( {
where : {
trace_id : traceId ,
} ,
data : pull_request_db ,
} )
}
2024-07-13 01:05:09 +00:00
}
2025-04-02 01:19:57 +00:00
res . json ( review . data . id )
} else {
// Don't create a dependent PR, just return an error response
const reason = foundInvalidHunk
? "invalid line ordering in hunks"
: "no valid review comments could be created"
console . log ( ` Cannot create review due to ${ reason } ` )
// Return a meaningful error response
return res . status ( 422 ) . json ( {
error : ` Cannot create review comments due to ${ reason } ` ,
message : "Please consider creating a dependent PR instead" ,
} )
2024-07-13 01:05:09 +00:00
}
2024-02-21 00:39:58 +00:00
}
} catch ( error : any ) {
console . log ( ` Error in /cfapi/suggest-pr-changes: ${ error } ` )
console . log ( ` Error message: ${ error . message } ` )
console . log ( ` Error stack: ${ error . stack } ` )
posthog . capture ( {
2025-01-06 22:01:20 +00:00
distinctId : req.userId ,
2024-02-21 00:39:58 +00:00
event : ` cfapi-suggest-pr-changes-failed-error ` ,
properties : {
error : error.message ,
stack : error.stack ,
} ,
} )
2025-02-21 05:18:55 +00:00
res . status ( 500 ) . send ( ` Error creating pull request: ${ error . message } ` )
2025-01-06 23:25:54 +00:00
}
2025-02-21 05:18:55 +00:00
}