Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions internal/ai/cost_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"

"github.com/thomas-vilte/matecommit/internal/cache"
Expand Down Expand Up @@ -118,6 +119,24 @@ func (w *CostAwareWrapper) WrapGenerate(
tokens, err := w.provider.CountTokens(ctx, prompt)
if err == nil {
inputTokens = tokens
} else {
inputTokens = len(prompt) / 4

msg := "failed to count tokens via API, using local estimation"
errStr := err.Error()

if strings.Contains(errStr, "not supported") || strings.Contains(errStr, "not found") {
slog.Debug(msg,
"provider", w.provider.GetProviderName(),
"model", w.provider.GetModelName(),
"reason", "model_not_supported_or_found")
} else {
slog.Warn(msg,
"provider", w.provider.GetProviderName(),
"model", w.provider.GetModelName(),
"estimated_tokens", inputTokens,
"error", err)
}
}

suggestedModel := w.modelSelector.SelectBestModel(command, inputTokens)
Expand Down
24 changes: 22 additions & 2 deletions internal/ai/gemini/commit_summarizer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,29 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m

var responseText string
if geminiResp, ok := resp.(*genai.GenerateContentResponse); ok {
log.Debug("formatResponse received GenerateContentResponse",
"candidates_count", len(geminiResp.Candidates))
responseText = formatResponse(geminiResp)
} else if s, ok := resp.(string); ok {
responseText = s
if len(responseText) > 0 {
preview := responseText
if len(responseText) > 100 {
preview = responseText[:100]
}
log.Debug("formatResponse result",
"response_length", len(responseText),
"response_preview", preview)
} else {
log.Debug("formatResponse result empty")
}
} else if str, ok := resp.(string); ok {
responseText = str
log.Debug("received string response", "length", len(str))
} else if respMap, ok := resp.(map[string]interface{}); ok {
log.Debug("received map response from cache, extracting text")
responseText = extractTextFromMap(respMap)
log.Debug("extracted text from map", "length", len(responseText))
} else {
log.Warn("unexpected response type", "type", fmt.Sprintf("%T", resp))
}

if responseText == "" {
Expand Down
43 changes: 39 additions & 4 deletions internal/ai/gemini/release_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ type ReleaseNotesGenerator struct {
}

type ReleaseNotesJSON struct {
Title string `json:"title"`
Summary string `json:"summary"`
Highlights []string `json:"highlights"`
Title string `json:"title"`
Summary string `json:"summary"`
Highlights []string `json:"highlights"`
Sections []struct {
Title string `json:"title"`
Items []string `json:"items"`
} `json:"sections"`
BreakingChanges []string `json:"breaking_changes"`
Contributors string `json:"contributors"`
}
Expand All @@ -47,12 +51,33 @@ func getReleaseNotesSchema() *genai.Schema {
Type: genai.TypeString,
Description: "2-3 sentences explaining the release focus in first person plural",
},
"sections": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"title": {
Type: genai.TypeString,
Description: "Section title (e.g. '🎨 UI/UX Improvements')",
},
"items": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeString,
},
Description: "List of items in this section",
},
},
Required: []string{"title", "items"},
},
Description: "Categorized sections of the release notes",
},
"highlights": {
Type: genai.TypeArray,
Items: &genai.Schema{
Type: genai.TypeString,
},
Description: "Array of highlights as strings",
Description: "Legacy flat list of highlights (keep empty if sections are used)",
},
"breaking_changes": {
Type: genai.TypeArray,
Expand Down Expand Up @@ -351,6 +376,16 @@ func (g *ReleaseNotesGenerator) parseJSONResponse(content string, release *model
Links: make(map[string]string),
}

if len(jsonNotes.Sections) > 0 {
notes.Sections = make([]models.ReleaseNotesSection, len(jsonNotes.Sections))
for i, s := range jsonNotes.Sections {
notes.Sections[i] = models.ReleaseNotesSection{
Title: s.Title,
Items: s.Items,
}
}
}

if jsonNotes.Contributors != "" && jsonNotes.Contributors != "N/A" {
notes.Links["Contributors"] = jsonNotes.Contributors
}
Expand Down
31 changes: 31 additions & 0 deletions internal/ai/gemini/release_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ func TestParseJSONResponse(t *testing.T) {
assert.Equal(t, "https://github.com/test/repo/graphs/contributors", notes.Links["Contributors"])
})

t.Run("parses JSON with semantic sections", func(t *testing.T) {
// Arrange
content := `{
"title": "Release v3.0.0",
"summary": "Semantic release",
"sections": [
{
"title": "🎨 UI Improvements",
"items": ["Dark Mode", "New Icons"]
},
{
"title": "🐛 Fixes",
"items": ["Crash on login"]
}
],
"highlights": [],
"breaking_changes": []
}`

// Act
notes, err := generator.parseJSONResponse(content, release)

// Assert
assert.NoError(t, err)
assert.Len(t, notes.Sections, 2)
assert.Equal(t, "🎨 UI Improvements", notes.Sections[0].Title)
assert.Equal(t, []string{"Dark Mode", "New Icons"}, notes.Sections[0].Items)
assert.Equal(t, "🐛 Fixes", notes.Sections[1].Title)
assert.Equal(t, []string{"Crash on login"}, notes.Sections[1].Items)
})

t.Run("handles invalid JSON", func(t *testing.T) {
// Arrange
content := `invalid json`
Expand Down
Loading
Loading