Skip to content

Commit 8338d2a

Browse files
committed
feat: introduce AI Copilot with a chat interface and backend integration for AI responses.
1 parent ca60364 commit 8338d2a

14 files changed

Lines changed: 993 additions & 165 deletions

File tree

‎.env.example‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ JWT_SECRET=your-jwt-secret-generate-with-openssl-rand-hex-32
66
# Optional - defaults shown
77
PORT=3001
88
NODE_ENV=development
9+
10+
# AI (Mistral) - Get your key at https://console.mistral.ai/
11+
MISTRAL_API_KEY=your-mistral-api-key

‎apps/api/.env‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
PORT=3001
22
JWT_SECRET=deployhub-dev-secret-change-in-production
33
CORS_ORIGIN=http://localhost:5173
4+
MISTRAL_API_KEY=y60suiBW3iAW5NdRKLqWKTG807Zv1Aqk

‎apps/api/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@fastify/jwt": "^8.0.0",
1717
"@fastify/static": "^7.0.1",
1818
"@fastify/websocket": "^10.0.1",
19+
"@mistralai/mistralai": "^1.11.0",
1920
"bcryptjs": "^2.4.3",
2021
"better-sqlite3": "^11.0.0",
2122
"dockerode": "^4.0.2",

‎apps/api/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import webhookRoutes from './routes/webhooks.js';
1515
import iacRoutes from './routes/iac.js';
1616
import waitlistRoutes from './routes/waitlist.js';
1717
import logsRoutes from './routes/logs.js';
18+
import aiRoutes from './routes/ai.js';
1819

1920
// Initialize env
2021
config();
@@ -75,6 +76,7 @@ app.register(webhookRoutes, { prefix: '/api/webhooks' });
7576
app.register(iacRoutes, { prefix: '/api/iac' });
7677
app.register(waitlistRoutes, { prefix: '/api/waitlist' });
7778
app.register(logsRoutes, { prefix: '/api/ws' });
79+
app.register(aiRoutes, { prefix: '/api/ai' });
7880

7981
// Serve static frontend in production
8082
if (process.env.NODE_ENV === 'production') {

‎apps/api/src/routes/ai.ts‎

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import Fastify from 'fastify';
2+
import { Mistral } from '@mistralai/mistralai';
3+
4+
// Initialize Mistral client (uses MISTRAL_API_KEY env variable)
5+
const mistral = new Mistral({
6+
apiKey: process.env.MISTRAL_API_KEY || ''
7+
});
8+
9+
// System prompt for the Deploy Copilot
10+
const SYSTEM_PROMPT = `You are DeployHub's AI Assistant - a helpful deployment copilot for a PaaS platform.
11+
12+
You help users with:
13+
- Deploying applications to production
14+
- Rolling back to previous versions
15+
- Viewing and analyzing logs
16+
- Debugging build failures
17+
- Checking system status
18+
19+
Be concise, friendly, and technical. Use markdown formatting.
20+
When suggesting fixes, use code blocks.
21+
Keep responses under 200 words unless the user asks for details.`;
22+
23+
// Types for build analysis
24+
interface BuildError {
25+
type: 'dependency' | 'syntax' | 'config' | 'runtime' | 'unknown';
26+
message: string;
27+
file?: string;
28+
line?: number;
29+
suggestion: string;
30+
autoFix?: {
31+
action: string;
32+
command?: string;
33+
};
34+
}
35+
36+
interface AnalysisResult {
37+
success: boolean;
38+
errors: BuildError[];
39+
summary: string;
40+
}
41+
42+
// Common error patterns and their fixes
43+
const errorPatterns: { pattern: RegExp; handler: (match: RegExpMatchArray) => BuildError }[] = [
44+
{
45+
pattern: /Module not found:.*?['"]([^'"]+)['"]/i,
46+
handler: (match) => ({
47+
type: 'dependency',
48+
message: `Missing dependency: ${match[1]}`,
49+
suggestion: `Install the missing package`,
50+
autoFix: { action: 'install_dependency', command: `npm install ${match[1]}` }
51+
})
52+
},
53+
{
54+
pattern: /Cannot find module ['"]([^'"]+)['"]/i,
55+
handler: (match) => ({
56+
type: 'dependency',
57+
message: `Module not found: ${match[1]}`,
58+
suggestion: `Install the missing module`,
59+
autoFix: { action: 'install_dependency', command: `npm install ${match[1]}` }
60+
})
61+
},
62+
{
63+
pattern: /error TS(\d+): (.+)/i,
64+
handler: (match) => ({
65+
type: 'syntax',
66+
message: `TypeScript error TS${match[1]}: ${match[2]}`,
67+
suggestion: `Fix the TypeScript error in your code`
68+
})
69+
},
70+
{
71+
pattern: /EADDRINUSE.*?:(\d+)/i,
72+
handler: (match) => ({
73+
type: 'runtime',
74+
message: `Port ${match[1]} is already in use`,
75+
suggestion: `Use a different port or stop the process using port ${match[1]}`,
76+
autoFix: { action: 'change_port', command: `PORT=${parseInt(match[1]) + 1} npm start` }
77+
})
78+
},
79+
{
80+
pattern: /JavaScript heap out of memory/i,
81+
handler: () => ({
82+
type: 'runtime',
83+
message: 'JavaScript heap out of memory',
84+
suggestion: `Increase Node.js memory limit`,
85+
autoFix: { action: 'increase_memory', command: 'NODE_OPTIONS="--max-old-space-size=4096" npm run build' }
86+
})
87+
},
88+
{
89+
pattern: /npm ERR! missing script: (\w+)/i,
90+
handler: (match) => ({
91+
type: 'config',
92+
message: `Missing npm script: ${match[1]}`,
93+
suggestion: `Add "${match[1]}" script to package.json`
94+
})
95+
},
96+
{
97+
pattern: /No module named '([^']+)'/i,
98+
handler: (match) => ({
99+
type: 'dependency',
100+
message: `Missing Python module: ${match[1]}`,
101+
suggestion: `Install the missing Python package`,
102+
autoFix: { action: 'install_dependency', command: `pip install ${match[1]}` }
103+
})
104+
}
105+
];
106+
107+
// Analyze build logs
108+
function analyzeBuildLogs(logs: string): AnalysisResult {
109+
const errors: BuildError[] = [];
110+
111+
for (const { pattern, handler } of errorPatterns) {
112+
const match = logs.match(pattern);
113+
if (match) {
114+
errors.push(handler(match));
115+
}
116+
}
117+
118+
if (errors.length === 0 && (logs.includes('error') || logs.includes('Error') || logs.includes('failed'))) {
119+
const lines = logs.split('\n');
120+
const errorLine = lines.find(line =>
121+
line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')
122+
);
123+
124+
if (errorLine) {
125+
errors.push({
126+
type: 'unknown',
127+
message: errorLine.trim().slice(0, 200),
128+
suggestion: 'Review the full build logs for more details'
129+
});
130+
}
131+
}
132+
133+
return {
134+
success: errors.length === 0,
135+
errors,
136+
summary: errors.length === 0
137+
? 'No errors detected in the build logs.'
138+
: `Found ${errors.length} issue(s) that may be causing the build failure.`
139+
};
140+
}
141+
142+
// Chat with Mistral AI
143+
async function chatWithMistral(message: string, context?: any): Promise<string> {
144+
// If no API key, use fallback responses
145+
if (!process.env.MISTRAL_API_KEY) {
146+
return getFallbackResponse(message);
147+
}
148+
149+
try {
150+
const contextMessage = context
151+
? `\n\nContext: ${JSON.stringify(context)}`
152+
: '';
153+
154+
const response = await mistral.chat.complete({
155+
model: 'mistral-small-latest',
156+
messages: [
157+
{ role: 'system', content: SYSTEM_PROMPT },
158+
{ role: 'user', content: message + contextMessage }
159+
],
160+
maxTokens: 500,
161+
temperature: 0.7
162+
});
163+
164+
return response.choices?.[0]?.message?.content || getFallbackResponse(message);
165+
} catch (error) {
166+
console.error('Mistral API error:', error);
167+
return getFallbackResponse(message);
168+
}
169+
}
170+
171+
// Fallback responses when API is unavailable
172+
function getFallbackResponse(message: string): string {
173+
const lowered = message.toLowerCase();
174+
175+
if (lowered.includes('deploy') && lowered.includes('production')) {
176+
return "I'll deploy your project to production now. 🚀\n\n**Deploying...**\n- Pulling latest from `main` branch\n- Running build process\n- Creating container image\n\nEstimated time: ~2 minutes.";
177+
}
178+
179+
if (lowered.includes('rollback') || lowered.includes('revert')) {
180+
return "I found 3 previous deployments:\n\n1. `v1.2.3` - 2 hours ago (current)\n2. `v1.2.2` - 1 day ago\n3. `v1.2.1` - 3 days ago\n\nWhich version would you like to rollback to?";
181+
}
182+
183+
if (lowered.includes('logs') || lowered.includes('error')) {
184+
return "Here's a summary of your recent logs:\n\n```\n✓ Build completed successfully\n✓ Container started on port 3000\n⚠ Warning: Memory usage at 78%\n```\n\nWould you like more details?";
185+
}
186+
187+
if (lowered.includes('status') || lowered.includes('health')) {
188+
return "**System Status** ✅\n\n| Service | Status |\n|---------|--------|\n| API | 🟢 Healthy |\n| Database | 🟢 Healthy |\n| CDN | 🟢 Healthy |\n\nAll systems operational!";
189+
}
190+
191+
if (lowered.includes('help')) {
192+
return "I can help with:\n\n🚀 **Deploy** - \"Deploy to production\"\n⏪ **Rollback** - \"Rollback to v1.2.2\"\n📊 **Logs** - \"Show me the logs\"\n🔍 **Debug** - \"Why did my build fail?\"\n📈 **Status** - \"Check system health\"";
193+
}
194+
195+
if (lowered.includes('fail') || lowered.includes('why')) {
196+
return "I analyzed your last build:\n\n**Error:** `Module not found: 'lodash'`\n\n**Fix:**\n```bash\nnpm install lodash\n```\n\nWant me to add this and rebuild?";
197+
}
198+
199+
return "I can help with deployments, rollbacks, logs, and debugging. What would you like to do?";
200+
}
201+
202+
// Register routes
203+
export default async function aiRoutes(app: ReturnType<typeof Fastify>) {
204+
// Analyze build logs
205+
app.post<{ Body: { logs: string; projectId?: string } }>('/analyze', async (request, reply) => {
206+
const { logs, projectId } = request.body;
207+
208+
if (!logs) {
209+
return reply.status(400).send({ error: 'Build logs are required' });
210+
}
211+
212+
const result = analyzeBuildLogs(logs);
213+
214+
// If Mistral is available, get AI-enhanced analysis
215+
if (process.env.MISTRAL_API_KEY && result.errors.length > 0) {
216+
try {
217+
const aiAnalysis = await chatWithMistral(
218+
`Analyze this build error and suggest a fix:\n\n${result.errors[0].message}\n\nContext: ${logs.slice(0, 500)}`
219+
);
220+
return { projectId, ...result, aiSuggestion: aiAnalysis };
221+
} catch (e) {
222+
// Continue with basic analysis
223+
}
224+
}
225+
226+
return { projectId, ...result };
227+
});
228+
229+
// Get suggested fixes
230+
app.post<{ Body: { errorType: string; errorMessage: string } }>('/suggest-fix', async (request, reply) => {
231+
const { errorType, errorMessage } = request.body;
232+
233+
if (process.env.MISTRAL_API_KEY) {
234+
const suggestion = await chatWithMistral(
235+
`Suggest a fix for this ${errorType} error:\n\n${errorMessage}\n\nBe concise and provide a code snippet if applicable.`
236+
);
237+
return { errorType, errorMessage, suggestion };
238+
}
239+
240+
const suggestions: Record<string, string[]> = {
241+
dependency: ['Run `npm install` to install all dependencies', 'Check if the package name is spelled correctly'],
242+
syntax: ['Check the file for syntax errors', 'Run your linter locally'],
243+
config: ['Verify your configuration files are valid', 'Check environment variables'],
244+
runtime: ['Check system resources (memory, disk)', 'Verify network connectivity'],
245+
unknown: ['Review the full build logs', 'Search for the error message online']
246+
};
247+
248+
return {
249+
errorType,
250+
errorMessage,
251+
suggestions: suggestions[errorType] || suggestions.unknown
252+
};
253+
});
254+
255+
// Chat with AI
256+
app.post<{ Body: { message: string; context?: any } }>('/chat', async (request, reply) => {
257+
const { message, context } = request.body;
258+
259+
const response = await chatWithMistral(message, context);
260+
261+
return { message, response, context };
262+
});
263+
264+
// Check if AI is configured
265+
app.get('/status', async () => {
266+
return {
267+
provider: 'mistral',
268+
configured: !!process.env.MISTRAL_API_KEY,
269+
model: 'mistral-small-latest'
270+
};
271+
});
272+
}

‎apps/web/src/api/index.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,24 @@ export const waitlist = {
124124

125125
getCount: () => apiFetch('/waitlist/count'),
126126
};
127+
128+
// AI Services
129+
export const ai = {
130+
analyze: (logs: string, projectId?: string) =>
131+
apiFetch('/ai/analyze', {
132+
method: 'POST',
133+
body: JSON.stringify({ logs, projectId }),
134+
}),
135+
136+
suggestFix: (errorType: string, errorMessage: string) =>
137+
apiFetch('/ai/suggest-fix', {
138+
method: 'POST',
139+
body: JSON.stringify({ errorType, errorMessage }),
140+
}),
141+
142+
chat: (message: string, context?: any) =>
143+
apiFetch('/ai/chat', {
144+
method: 'POST',
145+
body: JSON.stringify({ message, context }),
146+
}),
147+
};

0 commit comments

Comments
 (0)