Skip to content

Commit 6268fb4

Browse files
committed
feat: Enhance AI Code Agent with multi-provider support
- Updated the AI Code Agent to support multiple AI providers: Claude, OpenAI, and Google Gemini. - Modified README.md to include setup instructions for new AI providers and their respective API keys. - Improved error handling and Slack notification for agent failures. - Added a new package.json for agent dependencies and updated GitHub Actions workflow to install these dependencies. - Enhanced documentation for AI provider configuration and quick setup instructions.
1 parent 922394c commit 6268fb4

5 files changed

Lines changed: 269 additions & 33 deletions

File tree

.github/agent/agent.js

Lines changed: 176 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,140 @@
11
import { execSync } from "child_process";
2-
import OpenAI from "openai";
32
import fs from "fs";
43

5-
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
4+
// Support multiple AI providers
5+
const provider = process.env.AI_PROVIDER || "claude"; // claude, openai, or gemini
66
const task = process.env.TASK || "Improve code quality";
77
const requester = process.env.REQUESTER || "unknown";
8+
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
9+
const runId = process.env.GITHUB_RUN_ID;
10+
11+
// Initialize AI client based on provider
12+
let aiClient = null;
13+
let aiModel = null;
14+
15+
if (provider === "claude") {
16+
// Claude (Anthropic) - FREE tier available!
17+
const { Anthropic } = await import("@anthropic-ai/sdk");
18+
const apiKey = process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY;
19+
if (!apiKey) {
20+
console.error("[AGENT] ANTHROPIC_API_KEY or CLAUDE_API_KEY not set");
21+
process.exit(1);
22+
}
23+
aiClient = new Anthropic({ apiKey });
24+
aiModel = process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022";
25+
console.log(`[AGENT] Using Claude (Anthropic) with model: ${aiModel}`);
26+
} else if (provider === "openai") {
27+
// OpenAI (original)
28+
const { default: OpenAI } = await import("openai");
29+
const apiKey = process.env.OPENAI_API_KEY;
30+
if (!apiKey) {
31+
console.error("[AGENT] OPENAI_API_KEY not set");
32+
process.exit(1);
33+
}
34+
aiClient = new OpenAI({ apiKey });
35+
aiModel = process.env.OPENAI_MODEL || "gpt-4o";
36+
console.log(`[AGENT] Using OpenAI with model: ${aiModel}`);
37+
} else if (provider === "gemini") {
38+
// Google Gemini - FREE tier available!
39+
const { GoogleGenerativeAI } = await import("@google/generative-ai");
40+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
41+
if (!apiKey) {
42+
console.error("[AGENT] GEMINI_API_KEY or GOOGLE_API_KEY not set");
43+
process.exit(1);
44+
}
45+
const genAI = new GoogleGenerativeAI(apiKey);
46+
aiModel = process.env.GEMINI_MODEL || "gemini-1.5-pro";
47+
aiClient = genAI.getGenerativeModel({ model: aiModel });
48+
console.log(`[AGENT] Using Google Gemini with model: ${aiModel}`);
49+
} else {
50+
console.error(`[AGENT] Unknown provider: ${provider}. Use: claude, openai, or gemini`);
51+
process.exit(1);
52+
}
853

954
function sh(cmd) {
1055
return execSync(cmd, { encoding: "utf8", stdio: "pipe" });
1156
}
1257

58+
// Send error notification to Slack
59+
async function notifySlackError(errorMessage, errorType = "Unknown error") {
60+
if (!webhookUrl) {
61+
console.log("[AGENT] No SLACK_WEBHOOK_URL configured, skipping notification");
62+
return;
63+
}
64+
65+
const errorEmoji = errorType.includes("quota") ? "💳" : "❌";
66+
const message = {
67+
text: `${errorEmoji} AICODE Agent Failed`,
68+
blocks: [
69+
{
70+
type: "section",
71+
text: {
72+
type: "mrkdwn",
73+
text: `${errorEmoji} *AICODE Agent Failed*\n\n*Task:* ${task}\n*Requested by:* ${requester}\n*Error:* ${errorType}\n\n${errorMessage}`
74+
}
75+
},
76+
{
77+
type: "section",
78+
text: {
79+
type: "mrkdwn",
80+
text: `<https://github.com/htilly/SlackONOS/actions/runs/${runId}|View GitHub Actions logs>`
81+
}
82+
}
83+
]
84+
};
85+
86+
try {
87+
const response = await fetch(webhookUrl, {
88+
method: "POST",
89+
headers: { "Content-Type": "application/json" },
90+
body: JSON.stringify(message)
91+
});
92+
if (!response.ok) {
93+
console.error(`[AGENT] Failed to send Slack notification: ${response.status}`);
94+
} else {
95+
console.log("[AGENT] Error notification sent to Slack");
96+
}
97+
} catch (err) {
98+
console.error(`[AGENT] Error sending Slack notification: ${err.message}`);
99+
}
100+
}
101+
102+
// Handle errors and exit gracefully
103+
async function handleError(error, errorType = "Unknown error") {
104+
let errorMessage = error.message || String(error);
105+
106+
// Format quota errors more clearly
107+
if (error.code === "insufficient_quota" || error.type === "insufficient_quota") {
108+
errorType = `${provider.toUpperCase()} API Quota Exceeded`;
109+
if (provider === "claude") {
110+
errorMessage = "You exceeded your Anthropic API quota. Check billing at https://console.anthropic.com/";
111+
} else if (provider === "openai") {
112+
errorMessage = "You exceeded your OpenAI API quota. Check billing at https://platform.openai.com/account/billing";
113+
} else {
114+
errorMessage = "You exceeded your API quota. Please check your plan and billing details.";
115+
}
116+
} else if (error.status === 429 || error.statusCode === 429) {
117+
errorType = `${provider.toUpperCase()} API Rate Limit`;
118+
errorMessage = "API rate limit exceeded. Please try again later.";
119+
} else if (error.status === 401 || error.statusCode === 401) {
120+
errorType = `${provider.toUpperCase()} API Authentication Failed`;
121+
if (provider === "claude") {
122+
errorMessage = "Invalid Anthropic API key. Please check your GitHub secrets (ANTHROPIC_API_KEY or CLAUDE_API_KEY).";
123+
} else if (provider === "openai") {
124+
errorMessage = "Invalid OpenAI API key. Please check your GitHub secrets (OPENAI_API_KEY).";
125+
} else {
126+
errorMessage = "Invalid API key. Please check your GitHub secrets.";
127+
}
128+
}
129+
130+
console.error(`[AGENT] ${errorType}: ${errorMessage}`);
131+
132+
// Send notification to Slack
133+
await notifySlackError(errorMessage, errorType);
134+
135+
process.exit(1);
136+
}
137+
13138
console.log(`[AGENT] Starting AI code agent for task: ${task}`);
14139
console.log(`[AGENT] Requested by: ${requester}`);
15140

@@ -56,14 +181,40 @@ Generate a safe, focused code change as a unified git diff. The diff will be app
56181
57182
Remember: Output ONLY the git diff, no explanations, no markdown code blocks, just the raw diff.`;
58183

59-
console.log("[AGENT] Calling OpenAI API...");
60-
const res = await client.chat.completions.create({
61-
model: "gpt-4o",
62-
temperature: 0.2,
63-
messages: [{ role: "user", content: prompt }],
64-
});
184+
// Call AI provider with unified interface
185+
async function callAI(promptText) {
186+
if (provider === "claude") {
187+
const response = await aiClient.messages.create({
188+
model: aiModel,
189+
max_tokens: 4096,
190+
temperature: 0.2,
191+
messages: [{ role: "user", content: promptText }],
192+
});
193+
return response.content[0].text;
194+
} else if (provider === "openai") {
195+
const response = await aiClient.chat.completions.create({
196+
model: aiModel,
197+
temperature: 0.2,
198+
messages: [{ role: "user", content: promptText }],
199+
});
200+
return response.choices[0].message.content;
201+
} else if (provider === "gemini") {
202+
const result = await aiClient.generateContent({
203+
contents: [{ role: "user", parts: [{ text: promptText }] }],
204+
generationConfig: { temperature: 0.2 },
205+
});
206+
return result.response.text();
207+
}
208+
}
65209

66-
const output = res.choices[0].message.content;
210+
console.log(`[AGENT] Calling ${provider.toUpperCase()} API...`);
211+
let output;
212+
try {
213+
output = await callAI(prompt);
214+
} catch (error) {
215+
await handleError(error, `${provider.toUpperCase()} API Error`);
216+
return; // Exit already handled
217+
}
67218

68219
// Extract diff from potential markdown code blocks
69220
let diff = output;
@@ -77,10 +228,9 @@ if (output.includes("```")) {
77228

78229
// Validate diff format
79230
if (!diff.includes("diff --git")) {
80-
console.error("[AGENT] Model did not return a valid diff");
81-
console.error("[AGENT] Output was:");
82-
console.error(output);
83-
process.exit(1);
231+
const errorMsg = `Model did not return a valid diff. Output was:\n\`\`\`\n${output.substring(0, 500)}\n\`\`\``;
232+
await handleError(new Error(errorMsg), "Invalid Diff Format");
233+
return;
84234
}
85235

86236
// Safety check: Ensure we're not touching forbidden files
@@ -94,16 +244,22 @@ const forbiddenPatterns = [
94244

95245
for (const pattern of forbiddenPatterns) {
96246
if (pattern.test(diff)) {
97-
console.error(`[AGENT] SAFETY VIOLATION: Attempted to modify forbidden file matching ${pattern}`);
98-
process.exit(1);
247+
await handleError(
248+
new Error(`Attempted to modify forbidden file matching ${pattern}`),
249+
"Safety Violation"
250+
);
251+
return;
99252
}
100253
}
101254

102255
// Count lines changed
103256
const linesChanged = (diff.match(/^[+-][^+-]/gm) || []).length;
104257
if (linesChanged > 300) {
105-
console.error(`[AGENT] SAFETY VIOLATION: Too many lines changed (${linesChanged} > 300)`);
106-
process.exit(1);
258+
await handleError(
259+
new Error(`Too many lines changed (${linesChanged} > 300). Maximum allowed is 300 lines.`),
260+
"Safety Violation"
261+
);
262+
return;
107263
}
108264

109265
console.log(`[AGENT] Generated diff with ${linesChanged} lines changed`);
@@ -115,10 +271,9 @@ try {
115271
sh("git apply /tmp/aicode.patch");
116272
console.log("[AGENT] Patch applied successfully");
117273
} catch (err) {
118-
console.error("[AGENT] Failed to apply patch:", err.message);
119-
console.error("[AGENT] Diff was:");
120-
console.error(diff);
121-
process.exit(1);
274+
const errorMsg = `Failed to apply patch: ${err.message}\n\nDiff preview:\n\`\`\`\n${diff.substring(0, 500)}\n\`\`\``;
275+
await handleError(new Error(errorMsg), "Patch Application Failed");
276+
return;
122277
}
123278

124279
// Show diff for logs

.github/agent/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"@anthropic-ai/sdk": "^0.27.0",
5+
"@google/generative-ai": "^0.21.0",
6+
"openai": "^4.70.0"
7+
}
8+
}
9+

.github/workflows/aicode-agent.yml

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,50 @@ jobs:
2727
- name: Install dependencies
2828
run: npm ci
2929

30+
- name: Install agent dependencies
31+
working-directory: .github/agent
32+
run: npm install
33+
3034
- name: Run AI Agent
35+
id: agent
3136
env:
37+
# AI Provider selection (claude, openai, or gemini)
38+
AI_PROVIDER: ${{ secrets.AI_PROVIDER || 'claude' }}
39+
# Claude/Anthropic (default, FREE tier available)
40+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
41+
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }}
42+
CLAUDE_MODEL: ${{ secrets.CLAUDE_MODEL || 'claude-3-5-sonnet-20241022' }}
43+
# OpenAI (fallback)
3244
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
45+
OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || 'gpt-4o' }}
46+
# Google Gemini (alternative, FREE tier available)
47+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
48+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
49+
GEMINI_MODEL: ${{ secrets.GEMINI_MODEL || 'gemini-1.5-pro' }}
50+
# Other
51+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
52+
GITHUB_RUN_ID: ${{ github.run_id }}
3353
TASK: ${{ github.event.client_payload.task }}
3454
REQUESTER: ${{ github.event.client_payload.requester }}
35-
run: node .github/agent/agent.js
55+
run: node --input-type=module .github/agent/agent.js
56+
continue-on-error: true
57+
58+
- name: Notify Slack on agent failure
59+
if: steps.agent.outcome != 'success'
60+
run: |
61+
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
62+
-H "Content-Type: application/json" \
63+
-d "{\"text\":\"❌ AICODE Agent failed before tests. <https://github.com/htilly/SlackONOS/actions/runs/${{ github.run_id }}|View logs>\"}"
3664
3765
- name: Run tests
3866
id: tests
67+
if: steps.agent.outcome == 'success'
3968
run: npm test
4069
continue-on-error: true
4170

4271
- name: Create PR if tests pass
4372
id: cpr
44-
if: steps.tests.outcome == 'success'
73+
if: steps.agent.outcome == 'success' && steps.tests.outcome == 'success'
4574
uses: peter-evans/create-pull-request@v6
4675
with:
4776
token: ${{ secrets.GITHUB_TOKEN }}
@@ -59,14 +88,14 @@ jobs:
5988
Review the changes carefully before merging.
6089
6190
- name: Notify Slack on success
62-
if: steps.tests.outcome == 'success'
91+
if: steps.agent.outcome == 'success' && steps.tests.outcome == 'success'
6392
run: |
6493
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
6594
-H "Content-Type: application/json" \
6695
-d "{\"text\":\"✅ AICODE PR created: <https://github.com/htilly/SlackONOS/pull/${{ steps.cpr.outputs.pull-request-number }}|#${{ steps.cpr.outputs.pull-request-number }}>\"}"
6796
68-
- name: Notify Slack on failure
69-
if: steps.tests.outcome != 'success'
97+
- name: Notify Slack on test failure
98+
if: steps.agent.outcome == 'success' && steps.tests.outcome != 'success'
7099
run: |
71100
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
72101
-H "Content-Type: application/json" \

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,16 +605,31 @@ aicode <task description>
605605
```
606606

607607
**GitHub Secrets (Repository Settings):**
608-
1. `OPENAI_API_KEY` - OpenAI API key for GPT-4 (for code generation)
609-
2. `SLACK_WEBHOOK_URL` - Incoming webhook URL for Slack notifications
608+
609+
**AI Provider (choose one):**
610+
- **Claude (Recommended - FREE tier!):** `ANTHROPIC_API_KEY` or `CLAUDE_API_KEY`
611+
- Get key: https://console.anthropic.com/ ($5 free credit!)
612+
- Set `AI_PROVIDER=claude` (optional, default)
613+
- **Google Gemini (FREE tier!):** `GEMINI_API_KEY` or `GOOGLE_API_KEY`
614+
- Get key: https://aistudio.google.com/app/apikey
615+
- Set `AI_PROVIDER=gemini`
616+
- **OpenAI (paid):** `OPENAI_API_KEY`
617+
- Get key: https://platform.openai.com/api-keys
618+
- Set `AI_PROVIDER=openai`
619+
620+
**Required:**
621+
- `SLACK_WEBHOOK_URL` - Incoming webhook URL for Slack notifications
610622

611623
**Setup Steps:**
612624
1. Go to GitHub Settings → Developer Settings → Personal Access Tokens
613625
2. Generate new token (classic) with `repo` scope
614626
3. Add token to your `config/config.json` as `githubToken`
615627
4. Create a Slack incoming webhook: https://api.slack.com/messaging/webhooks
616628
5. Add webhook URL to GitHub repository secrets as `SLACK_WEBHOOK_URL`
617-
6. Add OpenAI API key to GitHub repository secrets as `OPENAI_API_KEY`
629+
6. **Choose AI Provider:**
630+
- **Claude (Recommended):** Get key from https://console.anthropic.com/ → Add as `ANTHROPIC_API_KEY` → (Optional) Set `AI_PROVIDER=claude`
631+
- **Gemini:** Get key from https://aistudio.google.com/app/apikey → Add as `GEMINI_API_KEY` → Set `AI_PROVIDER=gemini`
632+
- **OpenAI:** Get key from https://platform.openai.com/api-keys → Add as `OPENAI_API_KEY` → Set `AI_PROVIDER=openai`
618633

619634
### Safety Features
620635

0 commit comments

Comments
 (0)