Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions packages/agents/reviewAgent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
66 changes: 66 additions & 0 deletions packages/agents/reviewAgent/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import dotenv from 'dotenv';
import { App, Octokit } from "octokit";
import { createNodeMiddleware } from "@octokit/webhooks";
import fs from "fs";
import http from "http";
import { WebhookEventDefinition } from "@octokit/webhooks/types";
import { generate_pr_reviews } from './nodes/generate_pr_reviews.js';
import { github_push_pr_reviews } from './nodes/github_push_pr_reviews.js';
import { github_pr_parser } from './nodes/github_pr_parser.js';

dotenv.config();
const appId = process.env.APP_ID as string;
const webhookSecret = process.env.WEBHOOK_SECRET as string;
const privateKeyPath = process.env.PRIVATE_KEY_PATH as string;

const privateKey = fs.readFileSync(privateKeyPath, "utf8");

const app = new App({
appId: appId,
privateKey: privateKey,
webhooks: {
secret: webhookSecret
},
});

const rules = [
"Do NOT provide general feedback, summaries, explanations of changes, or praises for making good additions.",
"Do NOT provide any advice that is not actionable or directly related to the changes.",
"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.",
"Keep comments concise and to the point. Every comment must highlight a specific issue and provide a clear and actionable solution to the developer.",
"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\"."
]

async function handlePullRequestOpened({
octokit,
payload,
}: {
octokit: Octokit;
payload: WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize">;
}) {
console.log(`Received a pull request event for #${payload.pull_request.number}`);

const prPayload = await github_pr_parser(octokit, payload);
const fileDiffReviews = await generate_pr_reviews(prPayload, rules);
await github_push_pr_reviews(app, prPayload, fileDiffReviews);
}

app.webhooks.on("pull_request.opened", handlePullRequestOpened);
app.webhooks.on("pull_request.synchronize", handlePullRequestOpened);

app.webhooks.onError((error) => {
console.error(error);
});


const port = 3050;
const host = 'localhost';
const path = "/api/webhook";
const localWebhookUrl = `http://${host}:${port}${path}`;

const middleware = createNodeMiddleware(app.webhooks, { path });

http.createServer(middleware).listen(port, () => {
console.log(`Server is listening for events at: ${localWebhookUrl}`);
console.log('Press Ctrl + C to quit.')
});
49 changes: 49 additions & 0 deletions packages/agents/reviewAgent/nodes/fetch_file_content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { sourcebot_context, sourcebot_pr_payload } from "../types.js";
import { z } from "zod";

// TODO: use original Sourcebot schemas instead of redefining here
const fileSourceResponseSchema = z.object({
source: z.string(),
language: z.string(),
});

const base64Decode = (base64: string): string => {
const binString = atob(base64);
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
}

export const fetch_file_content = async (pr_payload: sourcebot_pr_payload, filename: string): Promise<sourcebot_context> => {
console.log("Executing fetch_file_content");

const fileSourceRequest = {
fileName: filename,
repository: pr_payload.hostDomain + "/" + pr_payload.owner + "/" + pr_payload.repo,
}
console.log(JSON.stringify(fileSourceRequest, null, 2));

const response = await fetch('http://localhost:3000/api/source', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Org-Domain': '~'
},
body: JSON.stringify(fileSourceRequest)
});

if (!response.ok) {
throw new Error(`Failed to fetch file content: ${response.statusText}`);
}

const responseData = await response.json();
const fileSourceResponse = fileSourceResponseSchema.parse(responseData);
const fileContent = base64Decode(fileSourceResponse.source);

const fileContentContext: sourcebot_context = {
type: "file_content",
description: `The content of the file ${filename}`,
context: fileContent,
}

console.log("Completed fetch_file_content");
return fileContentContext;
}
44 changes: 44 additions & 0 deletions packages/agents/reviewAgent/nodes/generate_diff_review_prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { sourcebot_diff, sourcebot_context, sourcebot_diff_review_schema } from "../types.js";
import { zodToJsonSchema } from "zod-to-json-schema";

export const generate_diff_review_prompt = async (diff: sourcebot_diff, context: sourcebot_context[], rules: string[]) => {
console.log("Executing generate_diff_review_prompt");

const prompt = `
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
must conform to all of the rules defined below. The output format of your review must conform to the output format defined below.

# Input

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
is a sequence of lines each with a line number.

## Old Code Snippet

\`\`\`
${diff.oldSnippet}
\`\`\`

## New Code Snippet

\`\`\`
${diff.newSnippet}
\`\`\`

# Additional Context

${context.map(c => `${c.type}: ${c.description}\n\n${c.context}`).join("\n\n----------------------\n\n")}

# Rules

- ${rules.join("\n- ")}

# Output Format (JSON Schema)
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
the JSON object in a markdown code block.
${JSON.stringify(zodToJsonSchema(sourcebot_diff_review_schema), null, 2)}
`;

console.log("Completed generate_diff_review_prompt");
return prompt;
}
46 changes: 46 additions & 0 deletions packages/agents/reviewAgent/nodes/generate_pr_reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { sourcebot_pr_payload, sourcebot_diff_review, sourcebot_file_diff_review, sourcebot_context } from "../types.js";
import { generate_diff_review_prompt } from "./generate_diff_review_prompt.js";
import { invoke_diff_review_llm } from "./invoke_diff_review_llm.js";
import { fetch_file_content } from "./fetch_file_content.js";

export const generate_pr_reviews = async (pr_payload: sourcebot_pr_payload, rules: string[]): Promise<sourcebot_file_diff_review[]> => {
console.log("Executing generate_pr_reviews");

const file_diff_reviews: sourcebot_file_diff_review[] = [];
for (const file_diff of pr_payload.file_diffs) {
const reviews: sourcebot_diff_review[] = [];

for (const diff of file_diff.diffs) {
const fileContentContext = await fetch_file_content(pr_payload, file_diff.to);
const context: sourcebot_context[] = [
{
type: "pr_title",
description: "The title of the pull request",
context: pr_payload.title,
},
{
type: "pr_description",
description: "The description of the pull request",
context: pr_payload.description,
},
fileContentContext,
];

const prompt = await generate_diff_review_prompt(diff, context, rules);
console.log(prompt);

const diffReview = await invoke_diff_review_llm(prompt, file_diff.to);
reviews.push(diffReview);
}

if (reviews.length > 0) {
file_diff_reviews.push({
filename: file_diff.to,
reviews: reviews,
});
}
}

console.log("Completed generate_pr_reviews");
return file_diff_reviews;
}
Loading