mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[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:
committed by
intellij-monorepo-bot
parent
698250965f
commit
9b904afcca
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user