Swadhin Biswas Feature/ai code reviewer (#73) · 8/2/2026
105a287
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { execSync } from 'child_process';
import { Mistral } from '@mistralai/mistralai';
import { GoogleGenAI } from '@google/genai';
import dotenv from 'dotenv';
dotenv.config();
const GEMINI_API_KEY = process.env.GEMINI_KEY || process.env.GEMINI_API_KEY;
const AGENT_API_KEY = process.env.AGENT_API_KEY;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const PR_NUMBER = process.env.PR_NUMBER;
const REPO = process.env.REPO;
const BASE_SHA = process.env.BASE_SHA;
const HEAD_SHA = process.env.HEAD_SHA;
if (!GEMINI_API_KEY && !AGENT_API_KEY) {
console.log("No AI provider API key found (GEMINI_KEY or AGENT_API_KEY). Skipping AI review.");
process.exit(0);
}
if (!GITHUB_TOKEN) {
console.log("GITHUB_TOKEN is not set. Skipping AI review.");
process.exit(0);
}
async function run() {
try {
// 1. Get the diff
console.log(`Getting diff between ${BASE_SHA} and ${HEAD_SHA}`);
const diff = execSync(`git diff ${BASE_SHA} ${HEAD_SHA}`).toString();
if (!diff.trim()) {
console.log("No diff found. Skipping review.");
return;
}
if (diff.length > 50000) {
console.log("Diff is too large for AI review (max 50,000 characters). Skipping.");
return;
}
const prompt = `You are a senior software engineer conducting a code review on a pull request.
Review the following git diff and provide constructive, human-like, and professional feedback.
Focus on identifying logic errors, security issues, performance bottlenecks, and best practice violations.
If the code looks perfect, say so in a friendly way.
Important constraints:
- Do NOT output any internal reasoning, thoughts, or <think> tags.
- Output ONLY the final review in markdown format.
- Be concise but thorough.
Diff:
\`\`\`diff
${diff}
\`\`\`
`;
let reviewComment = "";
if (AGENT_API_KEY) {
console.log("Requesting review from Mistral Agent...");
const client = new Mistral({ apiKey: AGENT_API_KEY });
const messages = [{ "role": "user" as const, "content": prompt }];
const response = await client.beta.conversations.start({
agentId: 'ag_019f4ae0eb2e764aaa3f1dec318fc748',
agentVersion: 0,
inputs: messages as any,
});
reviewComment = (response as any)?.choices?.[0]?.message?.content || (response as any)?.message?.content || "";
if (!reviewComment) {
console.log("Could not find standard content in response. Fallback to full response.");
process.exit(1);
}
reviewComment += "\n\n— *AI Code Review (Powered by Mistral Agent)*";
} else if (GEMINI_API_KEY) {
console.log("Requesting review from Gemini 3.5 Flash...");
const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
const tools: any[] = [{ type: 'google_search' }];
// Fixed config: Using camelCase and nested thinkingConfig
const generationConfig = {
maxOutputTokens: 65536,
thinkingConfig: { thinkingBudget: 8192 },
};
const interaction = await ai.interactions.create({
model: 'models/gemini-3.5-flash',
input: prompt,
tools: tools,
generation_config: generationConfig as any, // the sdk still expects the property to be named generation_config
} as any);
const step = interaction.steps?.at(-1);
reviewComment = (step as any)?.text || (step as any)?.parts?.[0]?.text || "";
if (!reviewComment) {
console.error("No content received from Gemini.");
process.exit(1);
}
reviewComment += "\n\n— *AI Code Review (Powered by Gemini)*";
}
// 3. Post to GitHub (and handle deduplication)
console.log("Fetching existing reviews...");
const reviewsResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews`, {
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28'
}
});
if (reviewsResponse.ok) {
const reviews = await reviewsResponse.json();
// Look for a previous review made by our bot
const previousReview = reviews.find((r: any) => r.body && r.body.includes('AI Code Review (Powered by'));
if (previousReview) {
console.log(`Updating existing review #${previousReview.id}...`);
const updateResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews/${previousReview.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28'
},
body: JSON.stringify({ body: reviewComment })
});
if (!updateResponse.ok) {
const ghError = await updateResponse.text();
console.error(`GitHub API Error on update: ${updateResponse.status} - ${ghError}`);
process.exit(1);
}
console.log("Review successfully updated!");
return;
}
}
console.log("Posting new review to GitHub PR...");
const ghResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28'
},
body: JSON.stringify({
body: reviewComment,
event: 'COMMENT'
})
});
if (!ghResponse.ok) {
const ghError = await ghResponse.text();
console.error(`GitHub API Error on post: ${ghResponse.status} - ${ghError}`);
process.exit(1);
}
console.log("Review successfully posted!");
} catch (error) {
console.error("Error during AI review:", error);
process.exit(1);
}
}
run();