Skip to content

Commit a242a69

Browse files
committed
add review command logic and add logging for review agent to data cache dir
1 parent 871d410 commit a242a69

File tree

12 files changed

+121
-52
lines changed

12 files changed

+121
-52
lines changed

.env.development

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ AUTH_URL="http://localhost:3000"
2323
# AUTH_GOOGLE_CLIENT_ID=""
2424
# AUTH_GOOGLE_CLIENT_SECRET=""
2525

26+
#DATA_CACHE_DIR="" # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
27+
2628
# Email
2729
# EMAIL_FROM_ADDRESS="" # The from address for transactional emails.
2830
# SMTP_CONNECTION_URL="" # The SMTP connection URL for transactional emails.
@@ -51,6 +53,16 @@ REDIS_URL="redis://localhost:6379"
5153
# STRIPE_WEBHOOK_SECRET: z.string().optional(),
5254
# STRIPE_ENABLE_TEST_CLOCKS=false
5355

56+
# Agents
57+
58+
# GITHUB_APP_ID=
59+
# GITHUB_APP_PRIVATE_KEY_PATH=
60+
# GITHUB_APP_WEBHOOK_SECRET=
61+
# OPENAI_API_KEY=
62+
REVIEW_AGENT_LOGGING_ENABLED=true
63+
REVIEW_AGENT_AUTO_REVIEW_ENABLED=false
64+
REVIEW_AGENT_REVIEW_COMMAND=review
65+
5466
# Misc
5567

5668
# Generated using:
@@ -79,9 +91,3 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
7991
# SOURCEBOT_TENANCY_MODE=single
8092

8193
# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=
82-
83-
# Used for agents
84-
# GITHUB_APP_ID=
85-
# GITHUB_APP_PRIVATE_KEY_PATH=
86-
# GITHUB_APP_WEBHOOK_SECRET=
87-
# OPENAI_API_KEY=

docs/docs/agents/review-agent.mdx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ Before you get started, make sure you have an OpenAPI account that you can creat
3030
- Webhook URL (**IMPORTANT**): You must set this to point to your Sourcebot deployment at /api/webhook (ex. https://sourcebot.aperture.com/api/webhook). Your Sourcebot deployment must be able to accept requests from GitHub
3131
(either github.com or your self-hosted enterprise server) for this to work. If you're running Sourcebot locally, you can [use smee](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#step-2-get-a-webhook-proxy-url) to [forward webhooks](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#step-6-start-your-server) to you local deployment.
3232
- Permissions
33-
- Pull requests: Read
33+
- Pull requests: Read & Write
34+
- Issues: Read & Write
3435
- Contents: Read
3536
- Events:
3637
- Pull request
38+
- Issue comment
3739
</Step>
3840
<Step title="Install the GitHub app in your organization">
3941
Navigate to your new GitHub app's page and press `Install`. You can find this in your [app settings](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings).
@@ -48,10 +50,19 @@ Before you get started, make sure you have an OpenAPI account that you can creat
4850
- `GITHUB_APP_PRIVATE_KEY_PATH`: The path to your app's private key. You can generate a private key file for your app in the [app settings](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/quickstart#navigate-to-your-app-settings)
4951
![GitHub App Private Key](/images/github_app_private_key.png)
5052
- `OPENAI_API_KEY`: Your OpenAI API key
53+
- `REVIEW_AGENT_AUTO_REVIEW_ENABLED` (default: `false`): If enabled, the review agent will automatically review any new or updated PR. If disabled, you must invoke it using the command defined by `REVIEW_AGENT_REVIEW_COMMAND`
54+
- `REVIEW_AGENT_REVIEW_COMMAND` (default: `review`): The command that invokes the review agent (ex. `/review`) when a user comments on the PR. Don't include the slash character in this value.
5155
</Step>
5256
<Step title="Verify configuration">
53-
Navigate to the agents page by pressing `Agents` in the Sourcebot nav menu. If you've configured your environment variables you'll see the following:
57+
Navigate to the agents page by pressing `Agents` in the Sourcebot nav menu. If you've configured your environment variables correctly you'll see the following:
5458

5559
![Review Agent Configured](/images/review_agent_configured.png)
5660
</Step>
57-
</Steps>
61+
</Steps>
62+
63+
# Using the agent
64+
65+
The review agent will not automatically review your PRs by default. To enable this feature, set the `REVIEW_AGENT_AUTO_REVIEW_ENABLED` environment variable to true.
66+
67+
You can invoke the review agent manually by commenting `/review` on the PR you'd like it to review. You can configure the command that triggers the agent by changing
68+
the `REVIEW_AGENT_REVIEW_COMMAND` environment variable.

packages/backend/src/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const env = createEnv({
2727
SOURCEBOT_INSTALL_ID: z.string().default("unknown"),
2828
NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default("unknown"),
2929

30+
DATA_CACHE_DIR: z.string(),
31+
3032
NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(),
3133

3234
FALLBACK_GITHUB_CLOUD_TOKEN: z.string().optional(),

packages/backend/src/index.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import path from 'path';
88
import { AppContext } from "./types.js";
99
import { main } from "./main.js"
1010
import { PrismaClient } from "@sourcebot/db";
11+
import { env } from "./env.js";
1112

1213
// Register handler for normal exit
1314
process.on('exit', (code) => {
@@ -36,22 +37,9 @@ process.on('unhandledRejection', (reason, promise) => {
3637
process.exit(1);
3738
});
3839

40+
console.log(process.cwd());
3941

40-
const parser = new ArgumentParser({
41-
description: "Sourcebot backend tool",
42-
});
43-
44-
type Arguments = {
45-
cacheDir: string;
46-
}
47-
48-
parser.add_argument("--cacheDir", {
49-
help: "Path to .sourcebot cache directory",
50-
required: true,
51-
});
52-
const args = parser.parse_args() as Arguments;
53-
54-
const cacheDir = args.cacheDir;
42+
const cacheDir = env.DATA_CACHE_DIR;
5543
const reposPath = path.join(cacheDir, 'repos');
5644
const indexPath = path.join(cacheDir, 'index');
5745

packages/web/src/app/api/(server)/webhook/route.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { env } from "@/env.mjs";
88
import { processGitHubPullRequest } from "@/features/agents/review-agent/app";
99
import { throttling } from "@octokit/plugin-throttling";
1010
import fs from "fs";
11+
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
1112

1213
let githubApp: App | undefined;
1314
if (env.GITHUB_APP_ID && env.GITHUB_APP_WEBHOOK_SECRET && env.GITHUB_APP_PRIVATE_KEY_PATH) {
@@ -42,6 +43,10 @@ function isPullRequestEvent(eventHeader: string, payload: unknown): payload is W
4243
return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize");
4344
}
4445

46+
function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"issue-comment-created"> {
47+
return eventHeader === "issue_comment" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && payload.action === "created";
48+
}
49+
4550
export const POST = async (request: NextRequest) => {
4651
const body = await request.json();
4752
const headers = Object.fromEntries(request.headers.entries());
@@ -56,6 +61,11 @@ export const POST = async (request: NextRequest) => {
5661
}
5762

5863
if (isPullRequestEvent(githubEvent, body)) {
64+
if (env.REVIEW_AGENT_AUTO_REVIEW_ENABLED === "false") {
65+
console.log('Review agent auto review (REVIEW_AGENT_AUTO_REVIEW_ENABLED) is disabled, skipping');
66+
return Response.json({ status: 'ok' });
67+
}
68+
5969
if (!body.installation) {
6070
console.error('Received github pull request event but installation is not present');
6171
return Response.json({ status: 'ok' });
@@ -64,7 +74,38 @@ export const POST = async (request: NextRequest) => {
6474
const installationId = body.installation.id;
6575
const octokit = await githubApp.getInstallationOctokit(installationId);
6676

67-
await processGitHubPullRequest(octokit, body);
77+
const pullRequest = body.pull_request as GitHubPullRequest;
78+
await processGitHubPullRequest(octokit, pullRequest);
79+
}
80+
81+
if (isIssueCommentEvent(githubEvent, body)) {
82+
const comment = body.comment.body;
83+
if (!comment) {
84+
console.warn('Received issue comment event but comment body is empty');
85+
return Response.json({ status: 'ok' });
86+
}
87+
88+
if (comment === `/${env.REVIEW_AGENT_REVIEW_COMMAND}`) {
89+
console.log('Review agent review command received, processing');
90+
91+
if (!body.installation) {
92+
console.error('Received github issue comment event but installation is not present');
93+
return Response.json({ status: 'ok' });
94+
}
95+
96+
const pullRequestNumber = body.issue.number;
97+
const repositoryName = body.repository.name;
98+
const owner = body.repository.owner.login;
99+
100+
const octokit = await githubApp.getInstallationOctokit(body.installation.id);
101+
const { data: pullRequest } = await octokit.rest.pulls.get({
102+
owner,
103+
repo: repositoryName,
104+
pull_number: pullRequestNumber,
105+
});
106+
107+
await processGitHubPullRequest(octokit, pullRequest);
108+
}
68109
}
69110
}
70111

packages/web/src/env.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const env = createEnv({
2727
AUTH_URL: z.string().url(),
2828
AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.default('true'),
2929

30+
DATA_CACHE_DIR: z.string(),
31+
3032
// Email
3133
SMTP_CONNECTION_URL: z.string().url().optional(),
3234
EMAIL_FROM_ADDRESS: z.string().email().optional(),
@@ -58,6 +60,9 @@ export const env = createEnv({
5860
GITHUB_APP_WEBHOOK_SECRET: z.string().optional(),
5961
GITHUB_APP_PRIVATE_KEY_PATH: z.string().optional(),
6062
OPENAI_API_KEY: z.string().optional(),
63+
REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'),
64+
REVIEW_AGENT_AUTO_REVIEW_ENABLED: booleanSchema.default('false'),
65+
REVIEW_AGENT_REVIEW_COMMAND: z.string().default('review'),
6166
},
6267
// @NOTE: Please make sure of the following:
6368
// - Make sure you destructure all client variables in
Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Octokit } from "octokit";
2-
import { WebhookEventDefinition } from "@octokit/webhooks/types";
32
import { generatePrReviews } from "@/features/agents/review-agent/nodes/generatePrReview";
43
import { githubPushPrReviews } from "@/features/agents/review-agent/nodes/githubPushPrReviews";
54
import { githubPrParser } from "@/features/agents/review-agent/nodes/githubPrParser";
65
import { env } from "@/env.mjs";
6+
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
7+
import path from "path";
8+
import fs from "fs";
79

810
const rules = [
911
"Do NOT provide general feedback, summaries, explanations of changes, or praises for making good additions.",
@@ -15,15 +17,27 @@ const rules = [
1517
"If there are no issues found on a line range, do NOT respond with any comments. This includes comments such as \"No issues found\" or \"LGTM\"."
1618
]
1719

18-
export async function processGitHubPullRequest(octokit: Octokit, payload: WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize">) {
19-
console.log(`Received a pull request event for #${payload.pull_request.number}`);
20+
export async function processGitHubPullRequest(octokit: Octokit, pullRequest: GitHubPullRequest) {
21+
console.log(`Received a pull request event for #${pullRequest.number}`);
2022

2123
if (!env.OPENAI_API_KEY) {
2224
console.error("OPENAI_API_KEY is not set, skipping review agent");
2325
return;
2426
}
2527

26-
const prPayload = await githubPrParser(octokit, payload);
27-
const fileDiffReviews = await generatePrReviews(prPayload, rules);
28+
let reviewAgentLogPath: string | undefined;
29+
if (env.REVIEW_AGENT_LOGGING_ENABLED) {
30+
const reviewAgentLogDir = path.join(env.DATA_CACHE_DIR, "review-agent");
31+
if (!fs.existsSync(reviewAgentLogDir)) {
32+
fs.mkdirSync(reviewAgentLogDir, { recursive: true });
33+
}
34+
35+
const timestamp = new Date().toLocaleString().replace(/[/: ,]/g, '');
36+
reviewAgentLogPath = path.join(reviewAgentLogDir, `review-agent-${pullRequest.number}-${timestamp}.log`);
37+
console.log(`Review agent logging to ${reviewAgentLogPath}`);
38+
}
39+
40+
const prPayload = await githubPrParser(octokit, pullRequest);
41+
const fileDiffReviews = await generatePrReviews(reviewAgentLogPath, prPayload, rules);
2842
await githubPushPrReviews(octokit, prPayload, fileDiffReviews);
2943
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { generateDiffReviewPrompt } from "@/features/agents/review-agent/nodes/g
33
import { invokeDiffReviewLlm } from "@/features/agents/review-agent/nodes/invokeDiffReviewLlm";
44
import { fetchFileContent } from "@/features/agents/review-agent/nodes/fetchFileContent";
55

6-
export const generatePrReviews = async (pr_payload: sourcebot_pr_payload, rules: string[]): Promise<sourcebot_file_diff_review[]> => {
6+
export const generatePrReviews = async (reviewAgentLogPath: string | undefined, pr_payload: sourcebot_pr_payload, rules: string[]): Promise<sourcebot_file_diff_review[]> => {
77
console.log("Executing generate_pr_reviews");
88

99
const file_diff_reviews: sourcebot_file_diff_review[] = [];
@@ -29,7 +29,7 @@ export const generatePrReviews = async (pr_payload: sourcebot_pr_payload, rules:
2929

3030
const prompt = await generateDiffReviewPrompt(diff, context, rules);
3131

32-
const diffReview = await invokeDiffReviewLlm(prompt);
32+
const diffReview = await invokeDiffReviewLlm(reviewAgentLogPath, prompt);
3333
reviews.push(diffReview);
3434
} catch (error) {
3535
console.error(`Error fetching file content for ${file_diff.to}: ${error}`);

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

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import { sourcebot_pr_payload, sourcebot_file_diff, sourcebot_diff } from "@/features/agents/review-agent/types";
2-
import { WebhookEventDefinition } from "@octokit/webhooks/types";
32
import parse from "parse-diff";
43
import { Octokit } from "octokit";
4+
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
55

6-
export const githubPrParser = async (octokit: Octokit, payload: WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize">): Promise<sourcebot_pr_payload> => {
6+
export const githubPrParser = async (octokit: Octokit, pullRequest: GitHubPullRequest): Promise<sourcebot_pr_payload> => {
77
console.log("Executing github_pr_parser");
88

9-
if (!payload.installation) {
10-
throw new Error("Installation not found in github payload");
11-
}
12-
139
let parsedDiff: parse.File[] = [];
1410
try {
15-
const diff = await octokit.request(payload.pull_request.patch_url);
11+
const diff = await octokit.request(pullRequest.diff_url);
1612
parsedDiff = parse(diff.data);
1713
} catch (error) {
1814
console.error("Error fetching diff: ", error);
@@ -56,14 +52,13 @@ export const githubPrParser = async (octokit: Octokit, payload: WebhookEventDefi
5652

5753
console.log("Completed github_pr_parser");
5854
return {
59-
title: payload.pull_request.title,
60-
description: payload.pull_request.body ?? "",
55+
title: pullRequest.title,
56+
description: pullRequest.body ?? "",
6157
hostDomain: "github.com",
62-
owner: payload.repository.owner.login,
63-
repo: payload.repository.name,
58+
owner: pullRequest.base.repo.owner.login,
59+
repo: pullRequest.base.repo.name,
6460
file_diffs: filteredSourcebotFileDiffs,
65-
number: payload.pull_request.number,
66-
head_sha: payload.pull_request.head.sha,
67-
installation_id: payload.installation!.id,
61+
number: pullRequest.number,
62+
head_sha: pullRequest.head.sha
6863
}
6964
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import OpenAI from "openai";
22
import { sourcebot_diff_review_schema, sourcebot_diff_review } from "@/features/agents/review-agent/types";
33
import { env } from "@/env.mjs";
4+
import fs from "fs";
45

5-
export const invokeDiffReviewLlm = async (prompt: string): Promise<sourcebot_diff_review> => {
6+
export const invokeDiffReviewLlm = async (reviewAgentLogPath: string | undefined, prompt: string): Promise<sourcebot_diff_review> => {
67
console.log("Executing invoke_diff_review_llm");
78

89
if (!env.OPENAI_API_KEY) {
@@ -14,7 +15,9 @@ export const invokeDiffReviewLlm = async (prompt: string): Promise<sourcebot_dif
1415
apiKey: env.OPENAI_API_KEY,
1516
});
1617

17-
console.log("Prompt: ", prompt);
18+
if (reviewAgentLogPath) {
19+
fs.appendFileSync(reviewAgentLogPath, `\n\nPrompt:\n${prompt}`);
20+
}
1821

1922
try {
2023
const completion = await openai.chat.completions.create({
@@ -25,7 +28,9 @@ export const invokeDiffReviewLlm = async (prompt: string): Promise<sourcebot_dif
2528
});
2629

2730
const openaiResponse = completion.choices[0].message.content;
28-
console.log("OpenAI response: ", openaiResponse);
31+
if (reviewAgentLogPath) {
32+
fs.appendFileSync(reviewAgentLogPath, `\n\nResponse:\n${openaiResponse}`);
33+
}
2934

3035
const diffReviewJson = JSON.parse(openaiResponse || '{}');
3136
const diffReview = sourcebot_diff_review_schema.safeParse(diffReviewJson);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { components } from "@octokit/openapi-types";
12
import { z } from "zod";
23

4+
export type GitHubPullRequest = components["schemas"]["pull-request"];
5+
36
export const sourcebot_diff_schema = z.object({
47
oldSnippet: z.string(),
58
newSnippet: z.string(),
@@ -21,8 +24,7 @@ export const sourcebot_pr_payload_schema = z.object({
2124
repo: z.string(),
2225
file_diffs: z.array(sourcebot_file_diff_schema),
2326
number: z.number(),
24-
head_sha: z.string(),
25-
installation_id: z.number(),
27+
head_sha: z.string()
2628
});
2729
export type sourcebot_pr_payload = z.infer<typeof sourcebot_pr_payload_schema>;
2830

supervisord.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ stdout_logfile_maxbytes=0
2222
redirect_stderr=true
2323

2424
[program:backend]
25-
command=./prefix-output.sh node packages/backend/dist/index.js --cacheDir %(ENV_DATA_CACHE_DIR)s
25+
command=./prefix-output.sh node packages/backend/dist/index.js
2626
autostart=true
2727
autorestart=true
2828
startretries=3

0 commit comments

Comments
 (0)