Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
8 changes: 7 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,10 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
# NODE_ENV=
# SOURCEBOT_TENANCY_MODE=single

# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=
# NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=

# Used for agents
# GITHUB_APP_ID=
# GITHUB_APP_PRIVATE_KEY_PATH=
# GITHUB_APP_WEBHOOK_SECRET=
# OPENAI_API_KEY=
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"private": true,
"workspaces": [
"packages/*"
"packages/*",
"packages/agents/*"
],
"scripts": {
"build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build",
Expand Down
136 changes: 136 additions & 0 deletions packages/agents/review-agent/.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.*
26 changes: 26 additions & 0 deletions packages/agents/review-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@sourcebot/review-agent",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"@octokit/webhooks": "^13.8.2",
"@octokit/webhooks-definitions": "octokit/webhooks",
"dotenv": "^16.5.0",
"octokit": "^4.1.3",
"openai": "^4.97.0",
"parse-diff": "^0.11.1",
"zod": "^3.24.4",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
"@types/node": "^22.15.8",
"smee-client": "^3.1.1",
"typescript": "^5.8.3"
}
}
62 changes: 62 additions & 0 deletions packages/agents/review-agent/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 path = "/api/webhook";

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

http.createServer(middleware).listen(port, () => {
console.log(`Http server for review agent running on port ${port} ${path}`);
});
13 changes: 13 additions & 0 deletions packages/agents/review-agent/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";

export const env = createEnv({
server: {
GITHUB_APP_ID: z.string(),
GITHUB_APP_WEBHOOK_SECRET: z.string(),
GITHUB_APP_PRIVATE_KEY_PATH: z.string(),
},
runtimeEnv: process.env,
emptyStringAsUndefined: true,
skipValidation: process.env.SKIP_ENV_VALIDATION === "1",
})
40 changes: 40 additions & 0 deletions packages/agents/review-agent/src/nodes/fetch_file_content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { sourcebot_context, sourcebot_pr_payload } from "../types.js";
import { fileSourceResponseSchema } from "@sourcebot/web/src/features/search/schemas.js";
import { base64Decode } from "@sourcebot/web/src/lib/utils.js";

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

const repoPath = pr_payload.hostDomain + "/" + pr_payload.owner + "/" + pr_payload.repo;
const fileSourceRequest = {
fileName: filename,
repository: repoPath,
}
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 for ${filename} from ${repoPath}: ${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;
}
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;
}
Loading