Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agentic review #7

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
1,751 changes: 988 additions & 763 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@dqbd/tiktoken": "^1.0.7",
"@langchain/core": "^0.3.19",
"@langchain/google-genai": "^0.1.4",
"@octokit/action": "^6.0.4",
"@octokit/plugin-retry": "^4.1.3",
"@octokit/plugin-throttling": "^6.1.0",
Expand All @@ -38,7 +40,6 @@
"p-retry": "^5.1.2"
},
"devDependencies": {
"@jest/globals": "^29.5.0",
"@types/node": "^20.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
Expand All @@ -53,7 +54,7 @@
"jest": "^27.2.5",
"js-yaml": "^4.1.0",
"prettier": "2.8.8",
"ts-jest": "^27.1.2",
"typescript": "^4.9.5"
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}
165 changes: 114 additions & 51 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,133 @@
import './fetch-polyfill'

import {info, error, setFailed, warning} from '@actions/core'
import pRetry from 'p-retry'
import {Options} from './options'
import {info, warning, error} from '@actions/core'
import {EragAPI} from './erag'
import {type Options} from './options'
import {ChatGoogleGenerativeAI} from '@langchain/google-genai'
import {PromptTemplate} from '@langchain/core/prompts'
import {StringOutputParser} from '@langchain/core/output_parsers'
import {Finding, FileInfo, PRSummaryInput} from './types'

export class Bot {
private readonly api: EragAPI | null = null
const PR_SUMMARY_TEMPLATE = `You are a code review assistant. Analyze this pull request and provide a clear, concise summary. This summary will be provided as a comment on the pull request and also used for context during the code review.

private readonly options: Options
Title: {title}
Description: {description}

constructor(options: Options) {
this.options = options
if (process.env.ERAG_ACCESS_TOKEN) {
this.api = new EragAPI(options.eragBaseUrl, options.model, options.eragProjectName, process.env.ERAG_ACCESS_TOKEN)
} else {
const err = "Unable to initialize the ERAG API, 'ERAG_ACCESS_TOKEN' environment variable is not available"
throw new Error(err)
}
Changes:
{changes}

Provide a summary covering:
1. The main purpose of the changes
2. Key components modified
3. Notable implementation details
4. Anything specific that the code reviewer should pay special attention to

Keep the summary focused and technical. Don't include code snippets, pleasantries, or unnecessary text.`

const REVIEW_TEMPLATE = `You are a code review assistant. Analyze the following changes and provide specific, actionable feedback.

Title: {title}
Description: {description}

PR Summary:
{summary}

Changes to Review:
{files}

For each potential issue found, provide:
1. A clear description of the concern. Don't include code snippets, only explain the issue and a potential solution in a few sentences.
2. The specific file and location
3. Any symbols (functions, classes, variables) that are relevant to the issue

IMPORTANT: Your response MUST be a valid JSON array of findings, each with these fields:
- description: string (clear explanation of the issue)
- file: string (full file path as appears in the input)
- lines?: { start: number, end: number } (location of the issue in the updated file)

Example response format:
[
{
"description": "The error handling could be improved by logging the error message.",
"file": "src/handler.ts",
"lines": { "start": 15, "end": 20 }
}
]

chat = async (message: string): Promise<string> => {
let res: string = ''
try {
res = await this.chat_(message)
return res
} catch (e: any) {
warning(`Failed to chat: ${e.message}, backtrace: ${e.stack}`)
return res
If there are no issues found, return an empty array.

Focus on:
- Potential bugs, regressions
- Maintainability issues
- Performance issues
- Simplification opportunities (less code lines the better)

Keep suggestions specific and actionable. Don't include general comments about coding style unless they impact maintainability.`

export class Bot {
private readonly api: EragAPI
private readonly gemini: ChatGoogleGenerativeAI

constructor(options: Options) {
// Initialize Gemini
if (!process.env.GOOGLE_API_KEY) {
throw new Error("Unable to initialize Gemini, 'GOOGLE_API_KEY' environment variable is not available")
}
if (!process.env.ERAG_ACCESS_TOKEN) {
throw new Error("Unable to initialize the ERAG API, 'ERAG_ACCESS_TOKEN' environment variable is not available")
}

this.gemini = new ChatGoogleGenerativeAI({
modelName: 'gemini-1.5-flash-latest',
maxOutputTokens: 1000,
apiKey: process.env.GOOGLE_API_KEY,
temperature: 0.4
})

// Initialize ERAG
this.api = new EragAPI(options.eragBaseUrl, 'bedrock-claude3.5-sonnet', options.eragProjectName, process.env.ERAG_ACCESS_TOKEN)
}

private readonly chat_ = async (message: string): Promise<string> => {
if (!message) {
return ''
}
async reviewBatch(summary: string, files: FileInfo[]): Promise<Finding[]> {
try {
const prompt = REVIEW_TEMPLATE.replace('{summary}', summary).replace(
'{files}',
files.map(file => `File: ${file.filename}\nPatch:\n${file.patch}`).join('\n---\n')
)

if (!this.api) {
setFailed('The ERAG API is not initialized')
return ''
const response = await this.api.sendMessage(prompt)
try {
// Extract JSON array from response using regex
const match = response.match(/\[.*\]/s)
if (!match) {
error(`No JSON array found in response: ${response}`)
throw new Error('No JSON array found in response')
}
return JSON.parse(match[0]) as Finding[]
} catch (parseError) {
error(`Failed to parse response as JSON: ${response}`)
throw new Error(`Invalid JSON response: ${parseError}`)
}
} catch (e) {
throw new Error(`Failed to review batch: ${e}`)
}
}

const start = Date.now()
let response: string = ''
async summarizePR(input: PRSummaryInput): Promise<string> {
try {
if (this.options.debug) {
info('::group::Sending message to erag')
info(`\n\n ${message}\n\n`)
info('::endgroup::')
}
// Create and run the chain
const prompt = PromptTemplate.fromTemplate(PR_SUMMARY_TEMPLATE)
const chain = prompt.pipe(this.gemini).pipe(new StringOutputParser())

response = await pRetry(() => this.api!.sendMessage(message), {
retries: this.options.eragRetries
const summary = await chain.invoke({
title: input.title,
description: input.description,
changes: input.changes.map(change => `File: ${change.filename}\nPatch:\n${change.patch}`).join('\n---\n')
})

if (this.options.debug) {
info('::group::Received response from erag')
info(`\n\n ${response}\n\n`)
info('::endgroup::')
}
} catch (err: any) {
error(`Failed to send message to erag: ${err}`)
info(`Generated PR Summary: ${summary}`)
return summary
} catch (e: any) {
warning(`Failed to generate PR summary: ${e.message}, backtrace: ${e.stack}`)
throw e
}
const end = Date.now()
info(`erag sendMessage (including retries) response time: ${end - start} ms`)

return response
}
}
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {info, setFailed, warning} from '@actions/core'
import {Bot} from './bot'
import {Options} from './options'
import {Prompts} from './prompts'
import {codeReview} from './review'
import {codeReview} from './review_old'
import {handleReviewComment} from './review-comment'

async function run(): Promise<void> {
Expand Down
53 changes: 16 additions & 37 deletions src/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {type Inputs} from './inputs'

export class Prompts {
systemMessage = `$system_message

`
systemMessage = `$system_message\n`

summarize = `
Provide your final response in markdown with the following content:
Expand Down Expand Up @@ -307,38 +305,19 @@ $comment
\`\`\`
`

renderSummarizeFileDiff(inputs: Inputs): string {
const prompt = this.systemMessage + this.summarizeFileDiff
return inputs.render(prompt)
}

renderSummarizeChangesets(inputs: Inputs): string {
const prompt = this.systemMessage + this.summarizeChangesets
return inputs.render(prompt)
}

renderSummarize(inputs: Inputs): string {
const prompt = this.systemMessage + this.summarizePrefix + this.summarize
return inputs.render(prompt)
}

renderSummarizeShort(inputs: Inputs): string {
const prompt = this.systemMessage + this.summarizePrefix + this.summarizeShort
return inputs.render(prompt)
}

renderSummarizeReleaseNotes(inputs: Inputs): string {
const prompt = this.systemMessage + this.summarizePrefix + this.summarizeReleaseNotes
return inputs.render(prompt)
}

renderComment(inputs: Inputs): string {
const prompt = this.systemMessage + this.comment
return inputs.render(prompt)
}

renderReviewFileDiff(inputs: Inputs): string {
const prompt = this.systemMessage + this.reviewFileDiff
return inputs.render(prompt)
}
renderComment = (inputs: Inputs) => `
You are a code review assistant. Continue this code review discussion thread.

Context:
${inputs.diff}

Previous comments:
${inputs.commentChain}

Latest comment:
${inputs.comment}

Provide a helpful, technical response that moves the discussion forward. If the discussion seems resolved, say so.
If you need more context, mention specific files or symbols that would help you understand better.
`
}
Loading