Skip to content

Commit d37daee

Browse files
committed
push review agent implementation
1 parent fb14142 commit d37daee

13 files changed

+1506
-28
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
.pnpm-debug.log*
9+
10+
# Diagnostic reports (https://nodejs.org/api/report.html)
11+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
*.lcov
25+
26+
# nyc test coverage
27+
.nyc_output
28+
29+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30+
.grunt
31+
32+
# Bower dependency directory (https://bower.io/)
33+
bower_components
34+
35+
# node-waf configuration
36+
.lock-wscript
37+
38+
# Compiled binary addons (https://nodejs.org/api/addons.html)
39+
build/Release
40+
41+
# Dependency directories
42+
node_modules/
43+
jspm_packages/
44+
45+
# Snowpack dependency directory (https://snowpack.dev/)
46+
web_modules/
47+
48+
# TypeScript cache
49+
*.tsbuildinfo
50+
51+
# Optional npm cache directory
52+
.npm
53+
54+
# Optional eslint cache
55+
.eslintcache
56+
57+
# Optional stylelint cache
58+
.stylelintcache
59+
60+
# Microbundle cache
61+
.rpt2_cache/
62+
.rts2_cache_cjs/
63+
.rts2_cache_es/
64+
.rts2_cache_umd/
65+
66+
# Optional REPL history
67+
.node_repl_history
68+
69+
# Output of 'npm pack'
70+
*.tgz
71+
72+
# Yarn Integrity file
73+
.yarn-integrity
74+
75+
# dotenv environment variable files
76+
.env
77+
.env.development.local
78+
.env.test.local
79+
.env.production.local
80+
.env.local
81+
82+
# parcel-bundler cache (https://parceljs.org/)
83+
.cache
84+
.parcel-cache
85+
86+
# Next.js build output
87+
.next
88+
out
89+
90+
# Nuxt.js build / generate output
91+
.nuxt
92+
dist
93+
94+
# Gatsby files
95+
.cache/
96+
# Comment in the public line in if your project uses Gatsby and not Next.js
97+
# https://nextjs.org/blog/next-9-1#public-directory-support
98+
# public
99+
100+
# vuepress build output
101+
.vuepress/dist
102+
103+
# vuepress v2.x temp and cache directory
104+
.temp
105+
.cache
106+
107+
# vitepress build output
108+
**/.vitepress/dist
109+
110+
# vitepress cache directory
111+
**/.vitepress/cache
112+
113+
# Docusaurus cache and generated files
114+
.docusaurus
115+
116+
# Serverless directories
117+
.serverless/
118+
119+
# FuseBox cache
120+
.fusebox/
121+
122+
# DynamoDB Local files
123+
.dynamodb/
124+
125+
# TernJS port file
126+
.tern-port
127+
128+
# Stores VSCode versions used for testing VSCode extensions
129+
.vscode-test
130+
131+
# yarn v2
132+
.yarn/cache
133+
.yarn/unplugged
134+
.yarn/build-state.yml
135+
.yarn/install-state.gz
136+
.pnp.*

packages/agents/reviewAgent/app.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import dotenv from 'dotenv';
2+
import { App, Octokit } from "octokit";
3+
import { createNodeMiddleware } from "@octokit/webhooks";
4+
import fs from "fs";
5+
import http from "http";
6+
import { WebhookEventDefinition } from "@octokit/webhooks/types";
7+
import { generate_pr_reviews } from './nodes/generate_pr_reviews.js';
8+
import { github_push_pr_reviews } from './nodes/github_push_pr_reviews.js';
9+
import { github_pr_parser } from './nodes/github_pr_parser.js';
10+
11+
dotenv.config();
12+
const appId = process.env.APP_ID as string;
13+
const webhookSecret = process.env.WEBHOOK_SECRET as string;
14+
const privateKeyPath = process.env.PRIVATE_KEY_PATH as string;
15+
16+
const privateKey = fs.readFileSync(privateKeyPath, "utf8");
17+
18+
const app = new App({
19+
appId: appId,
20+
privateKey: privateKey,
21+
webhooks: {
22+
secret: webhookSecret
23+
},
24+
});
25+
26+
const rules = [
27+
"Do NOT provide general feedback, summaries, explanations of changes, or praises for making good additions.",
28+
"Do NOT provide any advice that is not actionable or directly related to the changes.",
29+
"Focus solely on offering specific, objective insights based on the given context and refrain from making broad comments about potential impacts on the system or question intentions behind the changes.",
30+
"Keep comments concise and to the point. Every comment must highlight a specific issue and provide a clear and actionable solution to the developer.",
31+
"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\"."
32+
]
33+
34+
async function handlePullRequestOpened({
35+
octokit,
36+
payload,
37+
}: {
38+
octokit: Octokit;
39+
payload: WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize">;
40+
}) {
41+
console.log(`Received a pull request event for #${payload.pull_request.number}`);
42+
43+
const prPayload = await github_pr_parser(octokit, payload);
44+
const fileDiffReviews = await generate_pr_reviews(prPayload, rules);
45+
await github_push_pr_reviews(app, prPayload, fileDiffReviews);
46+
}
47+
48+
app.webhooks.on("pull_request.opened", handlePullRequestOpened);
49+
app.webhooks.on("pull_request.synchronize", handlePullRequestOpened);
50+
51+
app.webhooks.onError((error) => {
52+
console.error(error);
53+
});
54+
55+
56+
const port = 3050;
57+
const host = 'localhost';
58+
const path = "/api/webhook";
59+
const localWebhookUrl = `http://${host}:${port}${path}`;
60+
61+
const middleware = createNodeMiddleware(app.webhooks, { path });
62+
63+
http.createServer(middleware).listen(port, () => {
64+
console.log(`Server is listening for events at: ${localWebhookUrl}`);
65+
console.log('Press Ctrl + C to quit.')
66+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { sourcebot_context, sourcebot_pr_payload } from "../types.js";
2+
import { z } from "zod";
3+
4+
// TODO: use original Sourcebot schemas instead of redefining here
5+
const fileSourceResponseSchema = z.object({
6+
source: z.string(),
7+
language: z.string(),
8+
});
9+
10+
const base64Decode = (base64: string): string => {
11+
const binString = atob(base64);
12+
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
13+
}
14+
15+
export const fetch_file_content = async (pr_payload: sourcebot_pr_payload, filename: string): Promise<sourcebot_context> => {
16+
console.log("Executing fetch_file_content");
17+
18+
const fileSourceRequest = {
19+
fileName: filename,
20+
repository: pr_payload.hostDomain + "/" + pr_payload.owner + "/" + pr_payload.repo,
21+
}
22+
console.log(JSON.stringify(fileSourceRequest, null, 2));
23+
24+
const response = await fetch('http://localhost:3000/api/source', {
25+
method: 'POST',
26+
headers: {
27+
'Content-Type': 'application/json',
28+
'X-Org-Domain': '~'
29+
},
30+
body: JSON.stringify(fileSourceRequest)
31+
});
32+
33+
if (!response.ok) {
34+
throw new Error(`Failed to fetch file content: ${response.statusText}`);
35+
}
36+
37+
const responseData = await response.json();
38+
const fileSourceResponse = fileSourceResponseSchema.parse(responseData);
39+
const fileContent = base64Decode(fileSourceResponse.source);
40+
41+
const fileContentContext: sourcebot_context = {
42+
type: "file_content",
43+
description: `The content of the file ${filename}`,
44+
context: fileContent,
45+
}
46+
47+
console.log("Completed fetch_file_content");
48+
return fileContentContext;
49+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { sourcebot_diff, sourcebot_context, sourcebot_diff_review_schema } from "../types.js";
2+
import { zodToJsonSchema } from "zod-to-json-schema";
3+
4+
export const generate_diff_review_prompt = async (diff: sourcebot_diff, context: sourcebot_context[], rules: string[]) => {
5+
console.log("Executing generate_diff_review_prompt");
6+
7+
const prompt = `
8+
You are an expert software engineer that excells at reviewing code changes. Given the input, additional context, and rules defined below, review the code changes and provide a detailed review. The review you provide
9+
must conform to all of the rules defined below. The output format of your review must conform to the output format defined below.
10+
11+
# Input
12+
13+
The input is the old and new code snippets, which represent a single hunk from a git diff. The old code snippet is the code before the changes were made, and the new code snippet is the code after the changes were made. Each code snippet
14+
is a sequence of lines each with a line number.
15+
16+
## Old Code Snippet
17+
18+
\`\`\`
19+
${diff.oldSnippet}
20+
\`\`\`
21+
22+
## New Code Snippet
23+
24+
\`\`\`
25+
${diff.newSnippet}
26+
\`\`\`
27+
28+
# Additional Context
29+
30+
${context.map(c => `${c.type}: ${c.description}\n\n${c.context}`).join("\n\n----------------------\n\n")}
31+
32+
# Rules
33+
34+
- ${rules.join("\n- ")}
35+
36+
# Output Format (JSON Schema)
37+
The output must be a valid JSON object that conforms to the following JSON schema. Do NOT respond with anything other than the JSON object. Do NOT respond with
38+
the JSON object in a markdown code block.
39+
${JSON.stringify(zodToJsonSchema(sourcebot_diff_review_schema), null, 2)}
40+
`;
41+
42+
console.log("Completed generate_diff_review_prompt");
43+
return prompt;
44+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { sourcebot_pr_payload, sourcebot_diff_review, sourcebot_file_diff_review, sourcebot_context } from "../types.js";
2+
import { generate_diff_review_prompt } from "./generate_diff_review_prompt.js";
3+
import { invoke_diff_review_llm } from "./invoke_diff_review_llm.js";
4+
import { fetch_file_content } from "./fetch_file_content.js";
5+
6+
export const generate_pr_reviews = async (pr_payload: sourcebot_pr_payload, rules: string[]): Promise<sourcebot_file_diff_review[]> => {
7+
console.log("Executing generate_pr_reviews");
8+
9+
const file_diff_reviews: sourcebot_file_diff_review[] = [];
10+
for (const file_diff of pr_payload.file_diffs) {
11+
const reviews: sourcebot_diff_review[] = [];
12+
13+
for (const diff of file_diff.diffs) {
14+
const fileContentContext = await fetch_file_content(pr_payload, file_diff.to);
15+
const context: sourcebot_context[] = [
16+
{
17+
type: "pr_title",
18+
description: "The title of the pull request",
19+
context: pr_payload.title,
20+
},
21+
{
22+
type: "pr_description",
23+
description: "The description of the pull request",
24+
context: pr_payload.description,
25+
},
26+
fileContentContext,
27+
];
28+
29+
const prompt = await generate_diff_review_prompt(diff, context, rules);
30+
console.log(prompt);
31+
32+
const diffReview = await invoke_diff_review_llm(prompt, file_diff.to);
33+
reviews.push(diffReview);
34+
}
35+
36+
if (reviews.length > 0) {
37+
file_diff_reviews.push({
38+
filename: file_diff.to,
39+
reviews: reviews,
40+
});
41+
}
42+
}
43+
44+
console.log("Completed generate_pr_reviews");
45+
return file_diff_reviews;
46+
}

0 commit comments

Comments
 (0)