@@ -46,6 +46,71 @@ type (
4646 }
4747)
4848
49+ // getCommitSuggestionSchema returns the JSON schema for commit suggestions
50+ func getCommitSuggestionSchema () * genai.Schema {
51+ return & genai.Schema {
52+ Type : genai .TypeArray ,
53+ Items : & genai.Schema {
54+ Type : genai .TypeObject ,
55+ Required : []string {"title" , "desc" , "files" },
56+ Properties : map [string ]* genai.Schema {
57+ "title" : {
58+ Type : genai .TypeString ,
59+ Description : "Commit title (type(scope): message)" ,
60+ },
61+ "desc" : {
62+ Type : genai .TypeString ,
63+ Description : "Detailed explanation in first person" ,
64+ },
65+ "files" : {
66+ Type : genai .TypeArray ,
67+ Items : & genai.Schema {
68+ Type : genai .TypeString ,
69+ },
70+ Description : "Array of file paths as strings" ,
71+ },
72+ "analysis" : {
73+ Type : genai .TypeObject ,
74+ Required : []string {"overview" , "purpose" , "impact" },
75+ Properties : map [string ]* genai.Schema {
76+ "overview" : {Type : genai .TypeString },
77+ "purpose" : {Type : genai .TypeString },
78+ "impact" : {Type : genai .TypeString },
79+ },
80+ },
81+ "requirements" : {
82+ Type : genai .TypeObject ,
83+ Required : []string {"status" , "missing" , "completed_indices" , "suggestions" },
84+ Properties : map [string ]* genai.Schema {
85+ "status" : {
86+ Type : genai .TypeString ,
87+ Enum : []string {"full_met" , "partially_met" , "not_met" },
88+ },
89+ "missing" : {
90+ Type : genai .TypeArray ,
91+ Items : & genai.Schema {
92+ Type : genai .TypeString ,
93+ },
94+ },
95+ "completed_indices" : {
96+ Type : genai .TypeArray ,
97+ Items : & genai.Schema {
98+ Type : genai .TypeInteger ,
99+ },
100+ },
101+ "suggestions" : {
102+ Type : genai .TypeArray ,
103+ Items : & genai.Schema {
104+ Type : genai .TypeString ,
105+ },
106+ },
107+ },
108+ },
109+ },
110+ },
111+ }
112+ }
113+
49114func NewGeminiCommitSummarizer (ctx context.Context , cfg * config.Config , onConfirmation ai.ConfirmationCallback ) (* GeminiCommitSummarizer , error ) {
50115 providerCfg , exists := cfg .AIProviders ["gemini" ]
51116 if ! exists || providerCfg .APIKey == "" {
@@ -97,13 +162,11 @@ func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, onConfir
97162
98163func (s * GeminiCommitSummarizer ) defaultGenerate (ctx context.Context , mName string , p string ) (interface {}, * models.TokenUsage , error ) {
99164 log := logger .FromContext (ctx )
100-
101165 log .Debug ("calling gemini API" ,
102166 "model" , mName ,
103167 "prompt_length" , len (p ))
104-
105- genConfig := GetGenerateConfig (mName , "application/json" )
106-
168+ schema := getCommitSuggestionSchema ()
169+ genConfig := GetGenerateConfig (mName , "application/json" , schema )
107170 resp , err := s .Client .Models .GenerateContent (ctx , mName , genai .Text (p ), genConfig )
108171 if err != nil {
109172 log .Error ("gemini API call failed" ,
@@ -116,23 +179,24 @@ func (s *GeminiCommitSummarizer) defaultGenerate(ctx context.Context, mName stri
116179 strings .Contains (errMsg , "resource exhausted" ) {
117180 return nil , nil , domainErrors .ErrGeminiQuotaExceeded .WithError (err )
118181 }
119-
120182 if strings .Contains (errMsg , "invalid" ) ||
121183 strings .Contains (errMsg , "unauthorized" ) ||
122184 strings .Contains (errMsg , "api key" ) {
123185 return nil , nil , domainErrors .ErrGeminiAPIKeyInvalid .WithError (err )
124186 }
125-
126187 return nil , nil , domainErrors .ErrAIGeneration .WithError (err )
127188 }
128-
129189 usage := extractUsage (resp )
130-
131- log .Debug ("gemini API response received" ,
132- "input_tokens" , usage .InputTokens ,
133- "output_tokens" , usage .OutputTokens ,
134- "candidates" , len (resp .Candidates ))
135-
190+ if usage != nil {
191+ log .Debug ("gemini API response received" ,
192+ "input_tokens" , usage .InputTokens ,
193+ "output_tokens" , usage .OutputTokens ,
194+ "candidates" , len (resp .Candidates ))
195+ } else {
196+ log .Debug ("gemini API response received" ,
197+ "candidates" , len (resp .Candidates ),
198+ "usage" , "nil" )
199+ }
136200 return resp , usage , nil
137201}
138202
@@ -216,42 +280,33 @@ func (s *GeminiCommitSummarizer) parseSuggestionsJSON(responseText string) ([]mo
216280 if responseText == "" {
217281 return nil , fmt .Errorf ("empty response text from AI" )
218282 }
219-
220- responseText = ExtractJSON (responseText )
221-
222283 var jsonSuggestions []CommitSuggestionJSON
223284 if err := json .Unmarshal ([]byte (responseText ), & jsonSuggestions ); err != nil {
224- // Log at default level (no context available here)
225285 return nil , fmt .Errorf ("error parsing JSON: %w" , err )
226286 }
227-
228287 suggestions := make ([]models.CommitSuggestion , 0 , len (jsonSuggestions ))
229288 for _ , js := range jsonSuggestions {
230289 suggestion := models.CommitSuggestion {
231290 CommitTitle : js .Title ,
232291 Explanation : js .Desc ,
233292 Files : js .Files ,
234293 }
235-
236294 if js .Analysis != nil {
237295 suggestion .CodeAnalysis = models.CodeAnalysis {
238296 ChangesOverview : js .Analysis .OverView ,
239297 PrimaryPurpose : js .Analysis .Purpose ,
240298 TechnicalImpact : js .Analysis .Impact ,
241299 }
242300 }
243-
244301 if js .Requirements != nil {
245302 suggestion .RequirementsAnalysis = models.RequirementsAnalysis {
246303 CriteriaStatus : models .CriteriaStatus (js .Requirements .Status ),
247304 MissingCriteria : js .Requirements .Missing ,
248305 ImprovementSuggestions : js .Requirements .Suggestions ,
249306 }
250307 }
251-
252308 suggestions = append (suggestions , suggestion )
253309 }
254-
255310 return suggestions , nil
256311}
257312
0 commit comments