Skip to content

Commit 4303ebd

Browse files
Gavin Williamsclaude
andcommitted
feat(web): add optional PR/MR summary comment to review agent
Adds REVIEW_AGENT_SUMMARY_ENABLED (default false) and REVIEW_AGENT_SUMMARY_MAX_LENGTH (default 250) env vars. When enabled, the review agent generates a concise markdown summary of the PR/MR changes and posts it as a top-level comment before pushing inline review comments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a6116ca commit 4303ebd

9 files changed

Lines changed: 109 additions & 3 deletions

File tree

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
REVIEW_AGENT_LOGGING_ENABLED: "true"
2828
REVIEW_AGENT_AUTO_REVIEW_ENABLED: "false"
2929
REVIEW_AGENT_REVIEW_COMMAND: review
30+
REVIEW_AGENT_SUMMARY_ENABLED: "false"
3031
depends_on:
3132
- postgres
3233
- redis

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ REDIS_URL="redis://localhost:6379"
4949
REVIEW_AGENT_LOGGING_ENABLED=true
5050
REVIEW_AGENT_AUTO_REVIEW_ENABLED=false
5151
REVIEW_AGENT_REVIEW_COMMAND=review
52+
REVIEW_AGENT_SUMMARY_ENABLED=false
5253

5354
# Misc
5455

docs/docs/configuration/environment-variables.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ The following environment variables allow you to configure your Sourcebot deploy
7474
| `REVIEW_AGENT_AUTO_REVIEW_ENABLED` | `false` | <p>Enables/disables automatic code reviews by the review agent.</p> |
7575
| `REVIEW_AGENT_LOGGING_ENABLED` | `true` | <p>Enables/disables logging for the review agent. Logs are saved in `DATA_CACHE_DIR/review-agent`</p> |
7676
| `REVIEW_AGENT_REVIEW_COMMAND` | `review` | <p>The command used to trigger a code review by the review agent.</p> |
77+
| `REVIEW_AGENT_SUMMARY_ENABLED` | `false` | <p>Enables/disables posting a concise summary comment on the PR/MR after each review.</p> |
78+
| `REVIEW_AGENT_SUMMARY_MAX_LENGTH` | `250` | <p>Maximum character length of the summary comment posted by the review agent.</p> |
7779

7880
### Overriding environment variables from the config
7981

docs/docs/features/agents/review-agent.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,5 @@ You can also trigger a review manually by commenting `/review` on any PR or MR.
145145
| `REVIEW_AGENT_REVIEW_COMMAND` | `review` | Comment command that triggers a manual review (without the `/`) |
146146
| `REVIEW_AGENT_MODEL` | first configured model | `displayName` of the language model to use for reviews |
147147
| `REVIEW_AGENT_LOGGING_ENABLED` | unset | Write prompt and response logs to disk for debugging |
148+
| `REVIEW_AGENT_SUMMARY_ENABLED` | `false` | Post a concise summary comment on the PR/MR after each review |
149+
| `REVIEW_AGENT_SUMMARY_MAX_LENGTH` | `250` | Maximum character length of the summary comment |

packages/shared/src/env.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ const options = {
240240
REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'),
241241
REVIEW_AGENT_AUTO_REVIEW_ENABLED: booleanSchema.default('false'),
242242
REVIEW_AGENT_REVIEW_COMMAND: z.string().default('review'),
243+
REVIEW_AGENT_SUMMARY_ENABLED: booleanSchema.default('false'),
244+
REVIEW_AGENT_SUMMARY_MAX_LENGTH: numberSchema.default(250),
243245

244246
ANTHROPIC_API_KEY: z.string().optional(),
245247
ANTHROPIC_AUTH_TOKEN: z.string().optional(),

packages/web/src/features/agents/review-agent/app.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Octokit } from "octokit";
22
import { Gitlab } from "@gitbeaker/rest";
33
import { generatePrReviews } from "@/features/agents/review-agent/nodes/generatePrReview";
4+
import { generatePrSummary } from "@/features/agents/review-agent/nodes/generatePrSummary";
45
import { githubPushPrReviews } from "@/features/agents/review-agent/nodes/githubPushPrReviews";
56
import { githubPrParser } from "@/features/agents/review-agent/nodes/githubPrParser";
67
import { getReviewAgentLogDir } from "@/features/agents/review-agent/nodes/invokeDiffReviewLlm";
@@ -55,7 +56,17 @@ export async function processGitHubPullRequest(octokit: Octokit, pullRequest: Gi
5556

5657
const prPayload = await githubPrParser(octokit, pullRequest);
5758
const fileDiffReviews = await generatePrReviews(reviewAgentLogPath, prPayload, rules);
58-
await githubPushPrReviews(octokit, prPayload, fileDiffReviews);
59+
60+
let summary: string | undefined;
61+
if (env.REVIEW_AGENT_SUMMARY_ENABLED) {
62+
try {
63+
summary = await generatePrSummary(prPayload);
64+
} catch (error) {
65+
logger.error(`Error generating PR summary: ${error}`);
66+
}
67+
}
68+
69+
await githubPushPrReviews(octokit, prPayload, fileDiffReviews, summary);
5970
}
6071

6172
export async function processGitLabMergeRequest(
@@ -70,5 +81,15 @@ export async function processGitLabMergeRequest(
7081

7182
const prPayload = await gitlabMrParser(gitlabClient, mrPayload, hostDomain);
7283
const fileDiffReviews = await generatePrReviews(reviewAgentLogPath, prPayload, rules);
73-
await gitlabPushMrReviews(gitlabClient, projectId, prPayload, fileDiffReviews);
84+
85+
let summary: string | undefined;
86+
if (env.REVIEW_AGENT_SUMMARY_ENABLED) {
87+
try {
88+
summary = await generatePrSummary(prPayload);
89+
} catch (error) {
90+
logger.error(`Error generating MR summary: ${error}`);
91+
}
92+
}
93+
94+
await gitlabPushMrReviews(gitlabClient, projectId, prPayload, fileDiffReviews, summary);
7495
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { sourcebot_pr_payload } from "@/features/agents/review-agent/types";
2+
import { getAISDKLanguageModelAndOptions, getConfiguredLanguageModels } from "@/features/chat/utils.server";
3+
import { env } from "@sourcebot/shared";
4+
import { generateText } from "ai";
5+
import { createLogger } from "@sourcebot/shared";
6+
7+
const logger = createLogger('generate-pr-summary');
8+
9+
export const generatePrSummary = async (prPayload: sourcebot_pr_payload): Promise<string> => {
10+
const maxSummaryLength = env.REVIEW_AGENT_SUMMARY_MAX_LENGTH;
11+
logger.debug("Executing generate_pr_summary");
12+
13+
const models = await getConfiguredLanguageModels();
14+
if (models.length === 0) {
15+
throw new Error("No language models are configured");
16+
}
17+
18+
let selectedModel = models[0];
19+
if (env.REVIEW_AGENT_MODEL) {
20+
const match = models.find((m) => m.displayName === env.REVIEW_AGENT_MODEL);
21+
if (match) {
22+
selectedModel = match;
23+
} else {
24+
logger.warn(`REVIEW_AGENT_MODEL="${env.REVIEW_AGENT_MODEL}" did not match any configured model displayName. Falling back to the first configured model.`);
25+
}
26+
}
27+
28+
const { model, providerOptions, temperature } = await getAISDKLanguageModelAndOptions(selectedModel);
29+
30+
const filesChanged = prPayload.file_diffs.map(f => f.to).join(", ");
31+
32+
const prompt = `Summarize the following pull request changes in ${maxSummaryLength} characters or fewer. Be concise and focus on what changed and why. You may use inline markdown (e.g. \`code\`, **bold**) but avoid headers, bullet lists, and block-level formatting.
33+
34+
PR Title: ${prPayload.title}
35+
PR Description: ${prPayload.description}
36+
Files changed: ${filesChanged}
37+
`;
38+
39+
const result = await generateText({
40+
model,
41+
system: `You are a code review assistant. Generate a concise markdown-compatible summary of pull request changes. The summary must be ${maxSummaryLength} characters or fewer. Avoid headers, bullet lists, and block-level formatting.`,
42+
prompt,
43+
providerOptions,
44+
temperature,
45+
});
46+
47+
const summary = result.text.trim().slice(0, maxSummaryLength);
48+
49+
logger.debug("Completed generate_pr_summary");
50+
return summary;
51+
};

packages/web/src/features/agents/review-agent/nodes/githubPushPrReviews.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@ import { createLogger } from "@sourcebot/shared";
44

55
const logger = createLogger('github-push-pr-reviews');
66

7-
export const githubPushPrReviews = async (octokit: Octokit, pr_payload: sourcebot_pr_payload, file_diff_reviews: sourcebot_file_diff_review[]) => {
7+
export const githubPushPrReviews = async (octokit: Octokit, pr_payload: sourcebot_pr_payload, file_diff_reviews: sourcebot_file_diff_review[], summary?: string) => {
88
logger.info("Executing github_push_pr_reviews");
99

10+
if (summary) {
11+
try {
12+
await octokit.rest.issues.createComment({
13+
owner: pr_payload.owner,
14+
repo: pr_payload.repo,
15+
issue_number: pr_payload.number,
16+
body: summary,
17+
});
18+
} catch (error) {
19+
logger.error(`Error posting PR summary comment: ${error}`);
20+
}
21+
}
22+
1023
try {
1124
for (const file_diff_review of file_diff_reviews) {
1225
for (const review of file_diff_review.reviews) {

packages/web/src/features/agents/review-agent/nodes/gitlabPushMrReviews.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,22 @@ export const gitlabPushMrReviews = async (
99
projectId: number,
1010
prPayload: sourcebot_pr_payload,
1111
fileDiffReviews: sourcebot_file_diff_review[],
12+
summary?: string,
1213
): Promise<void> => {
1314
logger.info("Executing gitlab_push_mr_reviews");
1415

16+
if (summary) {
17+
try {
18+
await gitlabClient.MergeRequestNotes.create(
19+
projectId,
20+
prPayload.number,
21+
summary,
22+
);
23+
} catch (error) {
24+
logger.error(`Error posting MR summary note: ${error}`);
25+
}
26+
}
27+
1528
if (!prPayload.diff_refs) {
1629
logger.error("diff_refs is missing from pr_payload, cannot post inline GitLab MR reviews");
1730
return;

0 commit comments

Comments
 (0)