[ghai] Refactor review buddy to use new Grazie Task API

(cherry picked from commit befe3f79a3b08249deba5dfa00d6421ce3ec632b)


(cherry picked from commit f06576e107ae447528d61054de44c63b9d69e358)

IJ-CR-148445

GitOrigin-RevId: 7e6feeca7007e740115024b0834029232c695a2b
This commit is contained in:
Chris Lemaire
2024-08-05 15:42:09 +02:00
committed by intellij-monorepo-bot
parent 698250965f
commit 9b904afcca
8 changed files with 98 additions and 532 deletions

View File

@@ -1,252 +1,5 @@
review.buddy.llm.summary.display.text=Write a summary that briefly describes the contents of the PR
review.buddy.llm.system.prompt=You are a concise professional Code Review Copilot that helps programmers to perform code reviews.\n\
I will send you a Pull Request that I need help reviewing.\n\
It will contain a list of files with their contents and diff information.\n\
\n\
You will then be asked to do one of the following:\n\
1. Write a text description with a summary of the Pull Request\n\
2. Write down a list of files from the Pull Request in the order that you suggest to use for reviewing: files that require more attention should go earlier\n\
3. For a specific file generate a summary that will help the reviewer to perform the review\n\
4. For a specific file generate a list of comments in JSON format: {"line_number": ..., "reasoning": ..., "comment": ...}.\n\
\n\
You MUST use simple language: the reader may be a non-native speaker. Do not use rare and complex wording.\n\
Never tell abstract non-specific problems, be short, concise and direct. \n\
List only important problems, never mind non-critical problems.\n\
In the reasoning field provide reasoning why problem is important and, where possible, demonstrate on what real data it can lead to problems and what can go wrong.\n\
\n\
Do you understand the guidelines?\n\
review.buddy.llm.system.response=Yes, I understand the guidelines:\n\
0. Use simple language without difficult words such as "utter importance", "inherently", etc.\n\
1. Provide a concise summary of the PR\n\
2. Suggest an order to review the changed files\n\
3. Provide summaries of individual files\n\
4. Provide a list of comments in JSON format\n\
5. Only list important issues\n\
6. Explain the importance of any issue found\n\
7. Double-check that only important issues are mentioned\n\
8. Do not provide abstract/non-specific statements\n\
review.buddy.llm.comment.output.format=In "reasoning" you can discuss why this particular issue is important and why people should take it into account.\n\
\n\
Your message should have the following format:\n\
```json\n\
{{\n\
"summary": ...,\n\
"comments": [\n\
{{"line_number": ..., "reasoning": ..., "comment": ...}},\n\
...\n\
]\n\
}}\n\
```\n\
review.buddy.llm.comment.output.format.all=In "reasoning" you can discuss why this particular issue is important and why people should take it into account.\n\
\n\
Your message should have the following format:\n\
```json\n\
[\n\
{{\n\
"filename": ...,\n\
"summary": ...,\n\
"comments": [\n\
{{"line_number": ..., "reasoning": ..., "comment": ...}},\n\
...\n\
]\n\
}},\n\
...\n\
]\n\
```\n\
review.buddy.llm.developer.comments.examples=Examples of good comments:\n\
- Rename variable Foo to Bar as it follows our project naming\n\
- You call function Foo incorrectly, you should pass one more parameter Param\n\
- A more clear way to do X would be to use streams, avoiding unnecessary loops\n\
\n\
Examples of bad comments:\n\
- Check if implementation of Foo is correct\n\
- Please write doc for the class C\n\
review.buddy.llm.summary.task=Below, you are given the contents of the Pull Request.\n\
It contains the list of files with content for each file before and after the changes.\n\
Write a summary that briefly describes the contents of the PR. The summary will be used by the reviewer to ease their work.\n\
The summary should answer the following questions:\n\
- What has changed in the codebase?\n\
- Which parts require special attention from the reviewer?\n\
\n\
After summarizing the PR, write a single sentence, condensing the summary for the reviewer to quickly get the idea.\n\
It should contain the main message in no more than 15 words in very simple language.\n\
review.buddy.llm.summary.guideline= Do not dive deep into the details for specific changes and files as you will do it later.\n\
Keep the summary concise. Do not add any unrelated information.\n\
Use simple language.\n\
You can do it, I believe in you!
review.buddy.llm.summary.output.format=Write the summary in the following format:\n\
\n\
## Pull Request Summary\n\
... here goes summary ...\n\
\n\
## Main idea\n\
The Main Idea written in a single sentence\n\
review.buddy.llm.merge.request.before.after=Pull Request is given below in the format "file name, version before, version after":\n\
{0}\n\
review.buddy.llm.file.before.after=```\n\
{0}\n\
```\n\
\n\
```\n\
{1}\n\
```\n\
review.buddy.llm.sort.files.task=Write down the list of files in the order that you suggest for the reviewer's inspection.\n\
review.buddy.llm.sort.files.guideline=More complex or critical changes should be reviewed first.\n\
Even if there are few changes in file, they can be critical and should be reviewed before the reviewer is tired.\n\
review.buddy.llm.list.of.files=Files in the PR are the following:\n\
{0}\n\
review.buddy.llm.sort.files.output.format=List the files in the JSON format:\n\
```json\n\
[\n\
file1,\n\
file2,\n\
...\n\
fileN\n\
]\n\
```\n\
review.buddy.llm.comment.file.reviewer.task=Now let's help reviewer to write a review for file {0}\n\
You will be given the contents of the file with line numbers below.\n\
Use the provided summary of the Pull Request to create a better review.\n\
\n\
First, write a short summary that the reviewer will read before diving deep into reviewing the file.\n\
Then highlight places in the file where reviewer should pay extra attention.\n\
Return your suggestions in JSON format.\n\
review.buddy.llm.comment.file.reviewer.guideline=These are the highlights for the REVIEWER of the Pull Request, not for the developer!\n\
Each highlight should point to a specific location in PR and describe what the reviewer should verify or pay attention to.\n\
In order to add a highlight, specify the line number where it should be added.\n\
The reviewer will then use your suggestions as GUIDANCE during the review.\n\
DOUBLE CHECK the suggestions.\n\
Suggest to inspect implementations of methods and classes only when you have STRONG SUSPICIONS about their CORRECTNESS or EFFICIENCY.\n\
\n\
You should highlight places in PR for the following reasons:\n\
- complex code\n\
- possible bugs\n\
- possible performance issues\n\
- possible violation of guidelines\n\
- potential security concerns\n\
- places for significant simplification or improvement\n\
\n\
Be very concise!\n\
Keep in mind that you are writing comments for the REVIEWER.\n\
Every highlight should be actionable.\n\
Based on the comment, the reviewer should understand how to proceed with the review.\n\
Use simple language.\n\
Avoid phrase "aligns with project goals" as it carries very little meaning.\n\
review.buddy.llm.reviewer.comments.examples=Examples of good comments:\n\
- Pay attention to the numThreads parameter and its impact on performance and resource utilization.\n\
- The comment is very good as changing number of threads from 1 to 4 could have broken the implementation.\n\
- Ensure all managed storages are closed correctly to avoid resource leaks.\n\
- It is a good comment because things like parallelization, data races, memory management can be tricky and require reviewers attention.\n\
- Class ViewerImplementation changed inheritance and the reviewer should verify the introduced architectural changes.\n\
- The comment is helpful because architectural changes are an important topic of discussion during reviews.\n\
\n\
Examples of bad comments:\n\
- Please review the implementation of the PhpRemoveCommentsTransformation class to ensure it handles comment removal in PHP files correctly.\n\
- The comment is bad because it suggests to check implementation of the existing class without any reason.\n\
- Examine the search for the FinallyEndInstruction to ensure it functions as intended.\n\
- Again, the comment is bad because it suggests examination of the class/method without a particular reason.\n\
- Developer should write documentation for the class AbstractMethodImplementation.\n\
- Comment is bad: you should NOT suggest to write documentation.\n\
- Check the logic for identifying and filtering comments, including edge cases and performance implications.\n\
- It is a bad comment because the logic is rather short and straightforward and does not require much attention from the reviewer.\n\
review.buddy.llm.comment.all.files.reviewer.task=Now let's help reviewer to write a review.\n\
You have can check the contents of all files in the PR above.\n\
Use them and the provided summary of the PR to create a better review.\n\
\n\
Write a short summary for each file that the reviewer will read before diving deep into reviewing the file.\n\
Then highlight places in each file where reviewer should pay extra attention.\n\
Return your suggestions in JSON format.\n\
review.buddy.llm.comment.file.developer.task=Now let's write a review for file {0}\n\
You will be given the contents of the file with line numbers below.\n\
Use the provided summary of the Pull Request to provide better review.\n\
First, write a short summary that will help reviewer to review this file.\n\
Then list the comments that you suggest to add in JSON format.\n\
review.buddy.llm.comment.file.developer.guideline=These are the comments for the DEVELOPER of the Pull Request, not for the reviewer!\n\
Each comment should point to a specific location in PR and describe what the developer should change or fix.\n\
In order to add a review, specify the line number where it should be added.\n\
The reviewer will then use the comments you suggest, edit them, add their own comments, and send the review to developer.\n\
\n\
Do not suggest to add documentation. Only add the review about possible problems and enhancements in the code such as:\n\
- performance issues\n\
- possible bugs\n\
- security concerns\n\
- unnecessary complexity\n\
- places for simplification or improvement\n\
- suggestion of refactorings\n\
- usage of language-specific syntax which developer can be unaware of\n\
\n\
Be very concise in your review!\n\
Keep in mind that you are writing comments for DEVELOPER not the REVIEWER.\n\
Every comment should be actionable. Suggestions to double-check something are not actionable.\n\
Based on the comment, the developer should understand what they should do and why it is necessary.\n\
When reviewing, pay attention to the new version of the file (after the change).\n\
Use simple language.\n\
review.buddy.llm.model.reply=Got it! What should I do next?\n\
review.buddy.llm.summary.stub=The summary of the PR:\n\
{0}\n\
review.buddy.llm.discussion.comment.task=I have question about the comment on line {0}.\n\
{1}\n\
review.buddy.llm.discussion.comment.guideline=Answer concisely.\n\
Do not use complex language.\n\
Your message is a comment to the reviewers question.\n\
Be very short, no more than 3 sentences.\n\
Do not use complex formatting.\n\
review.buddy.llm.discussion.summarize.task=Please write a comment to the DEVELOPER \
summarizing our discussion telling them what and why they have to fix or check in their implementation.\n\
Be concise and do not use any complex language. Do not mention that we discussed it. \
Just write a comment as if the reviewer was writing it.
tab.title.pr.ai.assistant=PR AI Assistant
review.buddy.llm.discussion.summarize.display=Summarize the discussion
ai.review.reviewWithAI.text=Review with AI Assistant
request.ai.review.in.review.toolwindow=Request AI review in Review Toolwindow
line=Line {0}1:
pull.request.summary=Pull Request Summary

View File

@@ -1,9 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ai.assistedReview
fun prepareMergeRequestData(data: List<ReviewFileAiData>): String =
data.joinToString(separator = "\n\n") {
it.rawLocalPath + "\n" +
"BEFORE changes:\n" + populateLineNumbers(it.contentBefore) + "\n\n" +
"AFTER changes:\n" + populateLineNumbers(it.contentAfter)
}

View File

@@ -1,23 +1,27 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ai.assistedReview
import com.intellij.ml.llm.core.chat.messages.*
import com.intellij.ml.llm.core.chat.session.ChatSession
import ai.grazie.model.llm.chat.v5.LLMChat
import ai.grazie.model.task.data.TaskStreamData
import com.intellij.ml.llm.core.grazieAPI.GrazieApiClient
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.PrivacySafeTaskCall
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.ReviewBuddyDiscussionCommentTaskCallBuilder
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.ReviewBuddyReviewAllFilesTaskCallBuilder
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.ReviewBuddyReviewFileTaskCallBuilder
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.ReviewBuddySummarizeDiscussionTaskCallBuilder
import com.intellij.ml.llm.grazieAPIAdapters.tasksFacade.ReviewBuddySummarizeTaskCallBuilder
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.platform.util.coroutines.childScope
import git4idea.repo.GitRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.jetbrains.plugins.github.ai.GithubAIBundle
import org.jetbrains.plugins.github.ai.assistedReview.llm.ask
import org.jetbrains.plugins.github.ai.assistedReview.llm.askRaw
import org.jetbrains.plugins.github.ai.assistedReview.llm.chatSession
import org.jetbrains.plugins.github.ai.assistedReview.llm.waitForCompletion
import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRAICommentChat
import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRAICommentChatMessage
@@ -29,13 +33,14 @@ class GHPRAiAssistantReviewService(
private val cs: CoroutineScope,
private val repository: GitRepository,
) {
private val commentChatInitialContexts = ConcurrentHashMap<AiComment, AiCommentChatInitialContext>()
private val commentChatInitialChats = ConcurrentHashMap<AiComment, LLMChat>()
private val commentChats = ConcurrentHashMap<AiComment, GHPRAICommentChat>()
suspend fun chatAboutComment(comment: AiComment): GHPRAICommentChat {
val initialContext = commentChatInitialContexts[comment] ?: error("Initial chat comment context not found")
val chatSession = createCommentChatSession(initialContext)
return commentChats.getOrPut(comment) { AiAssistantReviewCommentChat(comment, chatSession) }
private val grazieClient = GrazieApiClient.getClient()
fun chatAboutComment(comment: AiComment): GHPRAICommentChat {
val initialChat = commentChatInitialChats[comment] ?: error("Initial chat comment context not found")
return commentChats.getOrPut(comment) { AiAssistantReviewCommentChat(comment, initialChat) }
}
fun askChatToReview(
@@ -44,76 +49,52 @@ class GHPRAiAssistantReviewService(
): AiReviewResponse {
val state = MutableStateFlow<AiReviewResponseState>(AiReviewRequested)
val reviewRequestScope = cs.childScope("Review requested")
reviewRequestScope.launch {
val reviewSummaryCompletableMessage = getReviewSummary(files)
reviewSummaryCompletableMessage.stateFlow
.catch { state.emit(AiReviewFailed(error = it.localizedMessage)) }
.collect {
when (it) {
is ThinkingState -> Unit
is ReadyState -> {
val summary = reviewSummaryCompletableMessage.text.unwrap()
state.emit(AiReviewSummaryReceived(summary = summary))
reviewRequestScope.launch {
val fileReviews = if (runQueryPerFile) {
val reviewedFiles = mutableListOf<AiFileReviewResponse>()
files.forEach { file ->
reviewedFiles.add(getFileReview(summary, file))
state.emit(AiFileReviewsPartiallyReceived(summary, reviewedFiles.toList()))
}
reviewedFiles
}
else {
getAllFileReviews(summary, files)
}
state.emit(AiReviewCompleted(summary, fileReviews))
}
throw CancellationException("Summary received")
}
is CancelledState -> {
state.emit(AiReviewFailed(error = "Cancelled"))
throw CancellationException("Underlying flow cancelled")
}
is ErrorState -> {
state.emit(AiReviewFailed(error = it.text))
throw CancellationException("Error occurred in underlying flow")
}
else -> error("Unexpected state: $it")
val summary: String
try {
summary = reviewSummaryCompletableMessage.waitForCompletion()
} catch(_: CancellationException) {
state.emit(AiReviewFailed(error = "Cancelled"))
return@launch
} catch(e: Exception) {
state.emit(AiReviewFailed(error = e.localizedMessage))
return@launch
}
state.emit(AiReviewSummaryReceived(summary = summary))
reviewRequestScope.launch {
val fileReviews = if (runQueryPerFile) {
val reviewedFiles = mutableListOf<AiFileReviewResponse>()
files.forEach { file ->
reviewedFiles.add(getFileReview(summary, file))
state.emit(AiFileReviewsPartiallyReceived(summary, reviewedFiles.toList()))
}
reviewedFiles
}
else {
getAllFileReviews(summary, files)
}
state.emit(AiReviewCompleted(summary, fileReviews))
}
throw CancellationException("Summary received")
}
return AiReviewResponse(state)
}
private suspend fun getReviewSummary(files: List<ReviewFileAiData>): CompletableMessage {
val chatSession = chatSession(project) {
user(GithubAIBundle.message("review.buddy.llm.system.prompt"))
assistant(GithubAIBundle.message("review.buddy.llm.system.response"))
}
return chatSession.askRaw(
project,
displayPrompt = GithubAIBundle.message("review.buddy.llm.summary.display.text"),
prompt = ReviewBuddyPrompts.summarize(prepareMergeRequestData(files))
)
private suspend fun getReviewSummary(files: List<ReviewFileAiData>): Flow<TaskStreamData> {
val task = ReviewBuddySummarizeTaskCallBuilder(files.map { it.toTaskData() }).build()
return grazieClient.sendTaskRequest(project, task) ?: error("Failed to send task")
}
private suspend fun getFileReview(summary: String, file: ReviewFileAiData): AiFileReviewResponse {
val chatSession = chatSession(project) {
user(GithubAIBundle.message("review.buddy.llm.system.prompt"))
assistant(GithubAIBundle.message("review.buddy.llm.system.response"))
user(ReviewBuddyPrompts.summaryStub(summary))
assistant(ReviewBuddyPrompts.modelReplyToContinue())
}
val reviewFilePrompt = ReviewBuddyPrompts.fileReviewGuide(
file.rawLocalPath,
populateLineNumbers(file.contentBefore).orEmpty(),
populateLineNumbers(file.contentAfter).orEmpty()
)
val response = chatSession.ask(
project,
displayPrompt = "Review ${file.rawLocalPath}",
prompt = reviewFilePrompt
)
val taskBuilder = ReviewBuddyReviewFileTaskCallBuilder(summary, file.toTaskData())
val task = taskBuilder.build()
val response = grazieClient.sendTaskRequest(project, task)?.waitForCompletion() ?: error("Failed to send task")
val jsonResponse = extractJsonFromResponse(response)
val parsedResponse = try {
parseFileReview(jsonResponse)
@@ -122,10 +103,14 @@ class GHPRAiAssistantReviewService(
thisLogger().warn("Failed to parse JSON response: $jsonResponse", e)
FileReviewAiResponse(summary = "Failed to parse JSON response: ${e.localizedMessage}", comments = emptyList())
}
val aiComments = parsedResponse.comments.map { it.toModelViewComment() }
aiComments.forEach {
commentChatInitialContexts[it] = AiCommentChatInitialContext(summary, reviewFilePrompt, response, it)
val chat = LLMChat.build {
messages(taskBuilder.asChat())
assistant(response)
}
val aiComments = parsedResponse.comments.map { it.toModelComment() }
aiComments.forEach { commentChatInitialChats[it] = chat }
return AiFileReviewResponse(
file.rawLocalPath,
parsedResponse.summary,
@@ -135,18 +120,10 @@ class GHPRAiAssistantReviewService(
}
private suspend fun getAllFileReviews(summary: String, files: List<ReviewFileAiData>): List<AiFileReviewResponse> {
val chatSession = chatSession(project) {
user(GithubAIBundle.message("review.buddy.llm.system.prompt"))
assistant(GithubAIBundle.message("review.buddy.llm.system.response"))
user(ReviewBuddyPrompts.summarize(prepareMergeRequestData(files)))
assistant(summary)
}
val response = chatSession.ask(
project,
displayPrompt = "Reviewing files from the MR",
prompt = ReviewBuddyPrompts.allFileReviewGuides()
)
val jsonResponse = extractJsonFromResponse(response)
val task = ReviewBuddyReviewAllFilesTaskCallBuilder(summary, files.map { it.toTaskData() }).build()
val response = grazieClient.sendTaskRequest(project, task) ?: error("Failed to send task")
val jsonResponse = extractJsonFromResponse(response.waitForCompletion())
val parsedResponse = try {
parseAllFileReviews(jsonResponse)
}
@@ -160,7 +137,7 @@ class GHPRAiAssistantReviewService(
fileResponse.filename,
fileResponse.summary,
highlights = emptyList(),
comments = fileResponse.comments.map { it.toModelViewComment() }
comments = fileResponse.comments.map { it.toModelComment() }
)
}
}
@@ -171,52 +148,37 @@ class GHPRAiAssistantReviewService(
private fun parseAllFileReviews(jsonResponse: String): List<NamedFileReviewAiResponse> =
Json.decodeFromString<List<NamedFileReviewAiResponse>>(jsonResponse).map { it.sorted() }
private fun ReviewCommentAiResponse.toModelViewComment(): AiComment =
private fun ReviewCommentAiResponse.toModelComment(): AiComment =
AiComment(lineNumber, reasoning, comment)
private data class AiCommentChatInitialContext(
val summary: String,
val reviewFilePrompt: String,
val reviewFileResponse: String,
val aiComment: AiComment,
)
private suspend fun createCommentChatSession(initialContext: AiCommentChatInitialContext): ChatSession =
chatSession(project) {
user(GithubAIBundle.message("review.buddy.llm.system.prompt"))
assistant(GithubAIBundle.message("review.buddy.llm.system.response"))
user(ReviewBuddyPrompts.summaryStub(initialContext.summary))
assistant(ReviewBuddyPrompts.modelReplyToContinue())
user(initialContext.reviewFilePrompt)
assistant(initialContext.reviewFileResponse)
}
fun toLocalPath(filePath: FilePath): String {
return filePath.path.removePrefix(repository.root.path).removePrefix("/")
}
private inner class AiAssistantReviewCommentChat(
private val aiComment: AiComment,
private val chatSession: ChatSession,
initialChat: LLMChat,
) : GHPRAICommentChat {
private var chat = initialChat
override val messages: MutableSharedFlow<GHPRAICommentChatMessage> = MutableSharedFlow(replay = Int.MAX_VALUE)
override suspend fun sendMessage(message: String) {
val wrappedMessage = ReviewBuddyPrompts.discussionComment(aiComment.lineNumber, message)
processDiscussionMessage(message, wrappedMessage)
val task = ReviewBuddyDiscussionCommentTaskCallBuilder(aiComment.lineNumber, message, chat).build()
processDiscussionMessage(message, task)
}
override suspend fun summarizeDiscussion() {
val message = GithubAIBundle.message("review.buddy.llm.discussion.summarize.display")
val wrappedMessage = ReviewBuddyPrompts.discussionSummarize()
processDiscussionMessage(message, wrappedMessage)
val task = ReviewBuddySummarizeDiscussionTaskCallBuilder(chat).build()
processDiscussionMessage(message, task)
}
private suspend fun processDiscussionMessage(message: String, wrappedMessage: String) {
private suspend fun processDiscussionMessage(message: String, task: PrivacySafeTaskCall) {
messages.emit(GHPRAICommentChatMessage(message, isResponse = false))
val completableMessage = chatSession.askRaw(project, displayPrompt = message, prompt = wrappedMessage)
val response = grazieClient.sendTaskRequest(project, task) ?: error("Failed to send task")
cs.childScope("AI assistant response").launch {
val response = completableMessage.waitForCompletion()
val response = response.waitForCompletion()
messages.emit(GHPRAICommentChatMessage(response, isResponse = true))
}
}

View File

@@ -5,6 +5,7 @@ import com.intellij.collaboration.ui.codereview.diff.DiffLineLocation
import com.intellij.collaboration.util.ChangesSelection
import com.intellij.collaboration.util.RefComparisonChange
import com.intellij.diff.util.Side
import com.intellij.ml.llm.core.grazieAPI.tasks.vcs.reviewBuddy.ReviewBuddyFileData
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
@@ -25,7 +26,10 @@ data class ReviewFileAiData(
val changeToNavigate: RefComparisonChange,
val contentBefore: String?,
val contentAfter: String?
)
) {
fun toTaskData() =
ReviewBuddyFileData(rawLocalPath, contentBefore, contentAfter)
}
sealed interface AiReviewResponseState

View File

@@ -1,11 +1,6 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ai.assistedReview
import com.intellij.ml.llm.context.addLineNumbers
import org.jetbrains.plugins.github.ai.GithubAIBundle
fun buildPrompt(vararg promptComponents: String): String = promptComponents.joinToString("\n")
fun extractJsonFromResponse(response: String): String {
val extractJsonRegex = Regex("```json\n([\\s\\S]*?)\n```")
val matches = extractJsonRegex.findAll(response)
@@ -17,61 +12,3 @@ fun extractJsonFromResponse(response: String): String {
}
return jsonString
}
fun populateLineNumbers(content: String?) = content?.let { addLineNumbers(it) }
object ReviewBuddyPrompts {
fun summarize(mergeRequestContent: String): String = buildPrompt(
GithubAIBundle.message("review.buddy.llm.summary.task"),
GithubAIBundle.message("review.buddy.llm.summary.guideline"),
GithubAIBundle.message("review.buddy.llm.summary.output.format"),
GithubAIBundle.message("review.buddy.llm.merge.request.before.after", mergeRequestContent), // Formatting string
)
fun sortFiles(listOfFileNames: List<String>): String = buildPrompt(
GithubAIBundle.message("review.buddy.llm.sort.files.task"),
GithubAIBundle.message("review.buddy.llm.sort.files.guideline"),
GithubAIBundle.message("review.buddy.llm.list.of.files", listOfFileNames),
GithubAIBundle.message("review.buddy.llm.sort.files.output.format"),
)
fun fileReview(filename: String, codeBefore: String, codeAfter: String) = buildPrompt(
GithubAIBundle.message("review.buddy.llm.comment.file.developer.task", filename),
GithubAIBundle.message("review.buddy.llm.comment.file.developer.guideline"),
GithubAIBundle.message("review.buddy.llm.developer.comments.examples"),
GithubAIBundle.message("review.buddy.llm.comment.output.format"),
GithubAIBundle.message("review.buddy.llm.file.before.after", codeBefore, codeAfter),
)
fun fileReviewGuide(filename: String, codeBefore: String, codeAfter: String) = buildPrompt(
GithubAIBundle.message("review.buddy.llm.comment.file.reviewer.task", filename),
GithubAIBundle.message("review.buddy.llm.comment.file.reviewer.guideline"),
GithubAIBundle.message("review.buddy.llm.reviewer.comments.examples"),
GithubAIBundle.message("review.buddy.llm.comment.output.format"),
GithubAIBundle.message("review.buddy.llm.file.before.after", codeBefore, codeAfter),
)
fun allFileReviewGuides() = buildPrompt(
GithubAIBundle.message("review.buddy.llm.comment.all.files.reviewer.task"),
GithubAIBundle.message("review.buddy.llm.comment.file.reviewer.guideline"),
GithubAIBundle.message("review.buddy.llm.reviewer.comments.examples"),
GithubAIBundle.message("review.buddy.llm.comment.output.format.all"),
)
fun modelReplyToContinue() = buildPrompt(
GithubAIBundle.message("review.buddy.llm.model.reply")
)
fun summaryStub(summary: String) = buildPrompt(
GithubAIBundle.message("review.buddy.llm.summary.stub", summary),
)
fun discussionComment(lineNumber: Int, question: String) = buildPrompt(
GithubAIBundle.message("review.buddy.llm.discussion.comment.task", lineNumber, question),
GithubAIBundle.message("review.buddy.llm.discussion.comment.guideline"),
)
fun discussionSummarize() = buildPrompt(
GithubAIBundle.message("review.buddy.llm.discussion.summarize.task")
)
}

View File

@@ -1,37 +1,17 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ai.assistedReview.llm
import com.intellij.ml.llm.core.chat.messages.*
import com.intellij.ml.llm.core.models.openai.FormattedString
import com.intellij.ml.llm.privacy.trustedStringBuilders.privacyUnsafeDoNotUse
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapLatest
import ai.grazie.model.task.data.TaskStreamData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun CompletableMessage.waitForCompletion(): String {
var error = false
stateFlow
.mapLatest {
when (it) {
is ThinkingState -> Unit
is ReadyState -> {
text
throw CancellationException()
}
is CancelledState, is ErrorState -> {
error = true
throw CancellationException()
}
else -> error("Unexpected state: $it")
}
}
.collect()
if (error) {
error("Error happened during waiting for completion")
suspend fun Flow<TaskStreamData>.waitForCompletion(): String {
if (this is SharedFlow) {
error("Cannot wait for completion of a hot flow. It will never complete.")
}
return text.unwrap()
}
fun String.toFormattedString(): FormattedString = FormattedString(privacyUnsafeDoNotUse)
val result = StringBuilder()
collect { result.append(it.content) }
return result.toString()
}

View File

@@ -1,64 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.ai.assistedReview.llm
import com.intellij.ml.llm.core.chat.context.ChatContextItem
import com.intellij.ml.llm.core.chat.messages.ChatMessageAuthor
import com.intellij.ml.llm.core.chat.messages.CompletableMessage
import com.intellij.ml.llm.core.chat.messages.impl.UserMessage
import com.intellij.ml.llm.core.chat.session.*
import com.intellij.ml.llm.privacy.trustedStringBuilders.privacyUnsafeDoNotUse
import com.intellij.openapi.project.Project
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
suspend fun chatSession(project: Project, init: ChatSessionBuilder.() -> Unit): ChatSession {
val builder = ChatSessionBuilder(project)
init(builder)
return builder.build()
}
class ChatSessionBuilder(private val project: Project) {
private val initialChatMessages = mutableListOf<ChatContextItem>()
fun user(text: String) {
initialChatMessages.add(ChatContextItem(ChatMessageAuthor.User.toString(), text.privacyUnsafeDoNotUse))
}
fun assistant(text: String) {
initialChatMessages.add(ChatContextItem(ChatMessageAuthor.Assistant.toString(), text.privacyUnsafeDoNotUse))
}
suspend fun build(): ChatSession {
val chatCreationContext = ChatCreationContext(
ChatOrigin.AIAssistantTool,
ChatSessionStorage.SourceAction.NEW_CHAT,
extraItems = initialChatMessages
)
return ChatSessionHost.getInstance(project).createChatSession(chatCreationContext)
}
}
suspend fun ChatSession.ask(project: Project, displayPrompt: String, prompt: String): String =
askRaw(project, displayPrompt, prompt).waitForCompletion()
suspend fun ChatSession.askRaw(project: Project, displayPrompt: String, prompt: String): CompletableMessage {
var resultMessage: CompletableMessage? = null
sendMessage(
project = project,
kind = SimpleChat,
userMessage = UserMessage(
chat = this,
formattedDisplayText = displayPrompt.toFormattedString(),
formattedText = prompt.toFormattedString()
)
)
{
launch(Dispatchers.Default) {
it.textFlow.collect {
println(it)
}
}
resultMessage = it
}
return resultMessage ?: error("Failed to receive CompletableMessage")
}

View File

@@ -36,18 +36,18 @@ class GHPRAIReviewDataProvider(
it?.files?.map { it.comments }?.flatten().orEmpty()
}.stateInNow(cs, emptyList())
suspend fun discardComment(comment: GHPRAIComment) {
fun discardComment(comment: GHPRAIComment) {
comment.accepted.value = false
comment.rejected.value = true
}
suspend fun acceptComment(comment: GHPRAIComment) {
fun acceptComment(comment: GHPRAIComment) {
comment.accepted.value = true
comment.rejected.value = false
}
suspend fun getChat(comment: GHPRAIComment): GHPRAICommentChat {
val aiComment = comment.id as AiComment
fun getChat(comment: GHPRAIComment): GHPRAICommentChat {
val aiComment = comment.id
return service.chatAboutComment(aiComment)
}
@@ -87,7 +87,10 @@ class GHPRAIReviewDataProvider(
val filesRes = buildViewModel(sortedFilesResponse, files)
reviewState.value = GHPRAIReview(idea.convertToHtml(project), sum?.convertToHtml(project), filesRes, reviewCompleted = true)
}
is AiReviewFailed -> Unit
is AiReviewFailed -> {
println("Bad news...")
println(it.error)
}
}
}
}