|
| 1 | +import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai"; |
| 2 | +import { ConfigService } from "./ConfigService.js"; |
| 3 | +import { logger } from "../utils/logger.js"; |
| 4 | + |
| 5 | +export interface AIProvider { |
| 6 | + generateCommitMessage(diff: string): Promise<string>; |
| 7 | + analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string>; |
| 8 | + generateContent(prompt: string): Promise<string>; |
| 9 | +} |
| 10 | + |
| 11 | +export class AIService implements AIProvider { |
| 12 | + private genAI: GoogleGenerativeAI | null = null; |
| 13 | + private model: GenerativeModel | null = null; |
| 14 | + private configService: ConfigService; |
| 15 | + |
| 16 | + constructor(configService: ConfigService) { |
| 17 | + this.configService = configService; |
| 18 | + this.initClient(); |
| 19 | + } |
| 20 | + |
| 21 | + private initClient(): void { |
| 22 | + const config = this.configService.getConfig(); |
| 23 | + |
| 24 | + // Ensure we only init if the provider is gemini |
| 25 | + if (config.ai.provider === "gemini") { |
| 26 | + if (!config.ai.apiKey) { |
| 27 | + throw new Error("Gemini API Key is missing in .aigitrc"); |
| 28 | + } |
| 29 | + |
| 30 | + this.genAI = new GoogleGenerativeAI(config.ai.apiKey); |
| 31 | + this.model = this.genAI.getGenerativeModel({ |
| 32 | + model: config.ai.model || "gemini-1.5-flash", |
| 33 | + generationConfig: { |
| 34 | + temperature: 0.2, |
| 35 | + topP: 0.8, |
| 36 | + maxOutputTokens: 200, |
| 37 | + }, |
| 38 | + }); |
| 39 | + } else { |
| 40 | + throw new Error(`Unsupported AI provider: ${config.ai.provider}`); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * Generates a conventional commit message based on git diff |
| 46 | + */ |
| 47 | + public async generateCommitMessage(diff: string): Promise<string> { |
| 48 | + if (!this.model) { |
| 49 | + throw new Error("Gemini AI model not initialized. Check your config."); |
| 50 | + } |
| 51 | + |
| 52 | + const prompt = ` |
| 53 | + You are an expert software engineer. |
| 54 | + Generate a professional, concise conventional commit message based on this git diff: |
| 55 | + |
| 56 | + "${diff}" |
| 57 | + |
| 58 | + Instructions: |
| 59 | + 1. Use the format: <type>(<scope>): <description> |
| 60 | + 2. Common types: feat, fix, docs, style, refactor, test, chore. |
| 61 | + 3. Description should be in present tense and lowercase. |
| 62 | + 4. Return ONLY the commit message text. |
| 63 | + `; |
| 64 | + |
| 65 | + try { |
| 66 | + const result = await this.model.generateContent(prompt); |
| 67 | + const response = await result.response; |
| 68 | + const text = response.text().trim(); |
| 69 | + |
| 70 | + // Clean up potential markdown formatting if Gemini returns backticks |
| 71 | + return text.replace(/`/g, ""); |
| 72 | + } catch (error) { |
| 73 | + logger.error( |
| 74 | + `Gemini API Error: ${error instanceof Error ? error.message : String(error)}`, |
| 75 | + ); |
| 76 | + throw new Error("Failed to generate commit message via Gemini."); |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Analyzes merge conflicts and suggests resolutions |
| 82 | + */ |
| 83 | + public async analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string> { |
| 84 | + if (!this.model) throw new Error("AI Service not ready"); |
| 85 | + |
| 86 | + const conflictsWithContent = Object.entries(conflictFileContents) |
| 87 | + .map(([fileName, content]) => `FILE: ${fileName}\n${content}`) |
| 88 | + .join("\n\n"); |
| 89 | + |
| 90 | + const prompt = `Analyze the following files currently in a git conflict state and provide a high-level summary of the clashing changes. Include key differences and likely intent from both sides of each conflict marker block.\n\n${conflictsWithContent}`; |
| 91 | + |
| 92 | + try { |
| 93 | + const result = await this.model.generateContent(prompt); |
| 94 | + return result.response.text(); |
| 95 | + } catch (error) { |
| 96 | + logger.error( |
| 97 | + `Conflict Analysis Error: ${error instanceof Error ? error.message : String(error)}`, |
| 98 | + ); |
| 99 | + return "Could not analyze conflicts at this time."; |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + public async generateContent(prompt: string): Promise<string> { |
| 104 | + if (!this.model) { |
| 105 | + throw new Error("Gemini AI model not initialized. Check your config."); |
| 106 | + } |
| 107 | + |
| 108 | + try { |
| 109 | + const result = await this.model.generateContent(prompt); |
| 110 | + const response = await result.response; |
| 111 | + return response.text(); |
| 112 | + } catch (error) { |
| 113 | + const errorMsg = error instanceof Error ? error.message : String(error); |
| 114 | + logger.error( |
| 115 | + `AI generateContent Error: ${errorMsg}`, |
| 116 | + ); |
| 117 | + throw new Error("Failed to generate content via AI service."); |
| 118 | + } |
| 119 | + } |
| 120 | +} |
0 commit comments