Skip to content

Commit 984d1c8

Browse files
authored
Merge pull request #72 from thomas-vilte/dev
feat: Semantic release notes and robust AI token counting
2 parents 084fdb4 + dd6d118 commit 984d1c8

8 files changed

Lines changed: 167 additions & 211 deletions

File tree

internal/ai/cost_wrapper.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"log/slog"
8+
"strings"
89
"time"
910

1011
"github.com/thomas-vilte/matecommit/internal/cache"
@@ -118,6 +119,24 @@ func (w *CostAwareWrapper) WrapGenerate(
118119
tokens, err := w.provider.CountTokens(ctx, prompt)
119120
if err == nil {
120121
inputTokens = tokens
122+
} else {
123+
inputTokens = len(prompt) / 4
124+
125+
msg := "failed to count tokens via API, using local estimation"
126+
errStr := err.Error()
127+
128+
if strings.Contains(errStr, "not supported") || strings.Contains(errStr, "not found") {
129+
slog.Debug(msg,
130+
"provider", w.provider.GetProviderName(),
131+
"model", w.provider.GetModelName(),
132+
"reason", "model_not_supported_or_found")
133+
} else {
134+
slog.Warn(msg,
135+
"provider", w.provider.GetProviderName(),
136+
"model", w.provider.GetModelName(),
137+
"estimated_tokens", inputTokens,
138+
"error", err)
139+
}
121140
}
122141

123142
suggestedModel := w.modelSelector.SelectBestModel(command, inputTokens)

internal/ai/gemini/commit_summarizer_service.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,29 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m
232232

233233
var responseText string
234234
if geminiResp, ok := resp.(*genai.GenerateContentResponse); ok {
235+
log.Debug("formatResponse received GenerateContentResponse",
236+
"candidates_count", len(geminiResp.Candidates))
235237
responseText = formatResponse(geminiResp)
236-
} else if s, ok := resp.(string); ok {
237-
responseText = s
238+
if len(responseText) > 0 {
239+
preview := responseText
240+
if len(responseText) > 100 {
241+
preview = responseText[:100]
242+
}
243+
log.Debug("formatResponse result",
244+
"response_length", len(responseText),
245+
"response_preview", preview)
246+
} else {
247+
log.Debug("formatResponse result empty")
248+
}
249+
} else if str, ok := resp.(string); ok {
250+
responseText = str
251+
log.Debug("received string response", "length", len(str))
252+
} else if respMap, ok := resp.(map[string]interface{}); ok {
253+
log.Debug("received map response from cache, extracting text")
254+
responseText = extractTextFromMap(respMap)
255+
log.Debug("extracted text from map", "length", len(responseText))
256+
} else {
257+
log.Warn("unexpected response type", "type", fmt.Sprintf("%T", resp))
238258
}
239259

240260
if responseText == "" {

internal/ai/gemini/release_generator.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ type ReleaseNotesGenerator struct {
2626
}
2727

2828
type ReleaseNotesJSON struct {
29-
Title string `json:"title"`
30-
Summary string `json:"summary"`
31-
Highlights []string `json:"highlights"`
29+
Title string `json:"title"`
30+
Summary string `json:"summary"`
31+
Highlights []string `json:"highlights"`
32+
Sections []struct {
33+
Title string `json:"title"`
34+
Items []string `json:"items"`
35+
} `json:"sections"`
3236
BreakingChanges []string `json:"breaking_changes"`
3337
Contributors string `json:"contributors"`
3438
}
@@ -47,12 +51,33 @@ func getReleaseNotesSchema() *genai.Schema {
4751
Type: genai.TypeString,
4852
Description: "2-3 sentences explaining the release focus in first person plural",
4953
},
54+
"sections": {
55+
Type: genai.TypeArray,
56+
Items: &genai.Schema{
57+
Type: genai.TypeObject,
58+
Properties: map[string]*genai.Schema{
59+
"title": {
60+
Type: genai.TypeString,
61+
Description: "Section title (e.g. '🎨 UI/UX Improvements')",
62+
},
63+
"items": {
64+
Type: genai.TypeArray,
65+
Items: &genai.Schema{
66+
Type: genai.TypeString,
67+
},
68+
Description: "List of items in this section",
69+
},
70+
},
71+
Required: []string{"title", "items"},
72+
},
73+
Description: "Categorized sections of the release notes",
74+
},
5075
"highlights": {
5176
Type: genai.TypeArray,
5277
Items: &genai.Schema{
5378
Type: genai.TypeString,
5479
},
55-
Description: "Array of highlights as strings",
80+
Description: "Legacy flat list of highlights (keep empty if sections are used)",
5681
},
5782
"breaking_changes": {
5883
Type: genai.TypeArray,
@@ -351,6 +376,16 @@ func (g *ReleaseNotesGenerator) parseJSONResponse(content string, release *model
351376
Links: make(map[string]string),
352377
}
353378

379+
if len(jsonNotes.Sections) > 0 {
380+
notes.Sections = make([]models.ReleaseNotesSection, len(jsonNotes.Sections))
381+
for i, s := range jsonNotes.Sections {
382+
notes.Sections[i] = models.ReleaseNotesSection{
383+
Title: s.Title,
384+
Items: s.Items,
385+
}
386+
}
387+
}
388+
354389
if jsonNotes.Contributors != "" && jsonNotes.Contributors != "N/A" {
355390
notes.Links["Contributors"] = jsonNotes.Contributors
356391
}

internal/ai/gemini/release_generator_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,37 @@ func TestParseJSONResponse(t *testing.T) {
210210
assert.Equal(t, "https://github.com/test/repo/graphs/contributors", notes.Links["Contributors"])
211211
})
212212

213+
t.Run("parses JSON with semantic sections", func(t *testing.T) {
214+
// Arrange
215+
content := `{
216+
"title": "Release v3.0.0",
217+
"summary": "Semantic release",
218+
"sections": [
219+
{
220+
"title": "🎨 UI Improvements",
221+
"items": ["Dark Mode", "New Icons"]
222+
},
223+
{
224+
"title": "🐛 Fixes",
225+
"items": ["Crash on login"]
226+
}
227+
],
228+
"highlights": [],
229+
"breaking_changes": []
230+
}`
231+
232+
// Act
233+
notes, err := generator.parseJSONResponse(content, release)
234+
235+
// Assert
236+
assert.NoError(t, err)
237+
assert.Len(t, notes.Sections, 2)
238+
assert.Equal(t, "🎨 UI Improvements", notes.Sections[0].Title)
239+
assert.Equal(t, []string{"Dark Mode", "New Icons"}, notes.Sections[0].Items)
240+
assert.Equal(t, "🐛 Fixes", notes.Sections[1].Title)
241+
assert.Equal(t, []string{"Crash on login"}, notes.Sections[1].Items)
242+
})
243+
213244
t.Run("handles invalid JSON", func(t *testing.T) {
214245
// Arrange
215246
content := `invalid json`

0 commit comments

Comments
 (0)