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
54 changes: 46 additions & 8 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
cfg "github.com/thomas-vilte/matecommit/internal/config"
"github.com/thomas-vilte/matecommit/internal/git"
"github.com/thomas-vilte/matecommit/internal/i18n"
"github.com/thomas-vilte/matecommit/internal/logger"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/providers"
"github.com/thomas-vilte/matecommit/internal/services"
Expand All @@ -31,6 +32,8 @@ import (
)

func main() {
logger.Initialize(false, false)

app, err := initializeApp()
if err != nil {
log.Fatalf("Error starting the CLI: %v", err)
Expand Down Expand Up @@ -62,9 +65,16 @@ func initializeApp() (*cli.Command, error) {
}

ctx := context.Background()

gitService := git.NewGitService()

isCompletion := false
for _, arg := range os.Args {
if arg == "completion" || arg == "--generate-shell-completion" {
isCompletion = true
break
}
}

var commitAI ports.CommitSummarizer
var prAI ports.PRSummarizer
var issueAI ports.IssueContentGenerator
Expand All @@ -73,33 +83,46 @@ func initializeApp() (*cli.Command, error) {
onConfirmation := createConfirmationCallback(translations)

commitAI, err = providers.NewCommitSummarizer(ctx, cfgApp, onConfirmation)
if err != nil {
log.Printf("Warning: could not create CommitSummarizer: %v", err)
log.Println("AI is not configured. You can configure it with 'mate-commit config init'")
if err != nil && !isCompletion {
logger.Warn(ctx, "could not create CommitSummarizer",
"error", err)
logger.Info(ctx, "AI is not configured. You can configure it with 'mate-commit config init'")
}

prAI, err = providers.NewPRSummarizer(ctx, cfgApp, onConfirmation)
if err != nil {
log.Printf("Warning: could not create PRSummarizer: %v", err)
if !isCompletion {
logger.Warn(ctx, "could not create PRSummarizer",
"error", err)
}
prAI = nil
}

issueAI, err = providers.NewIssueContentGenerator(ctx, cfgApp, onConfirmation)
if err != nil {
log.Printf("Warning: could not create IssueContentGenerator: %v", err)
if !isCompletion {
logger.Warn(ctx, "could not create IssueContentGenerator",
"error", err)
}
issueAI = nil
}
}

vcsClient, err := providers.NewVCSClient(ctx, gitService, cfgApp)
if err != nil {
log.Printf("Warning: could not create VCS client: %v", err)
if !isCompletion {
logger.Warn(ctx, "could not create VCS client",
"error", err)
}
vcsClient = nil
}

ticketMgr, err := providers.NewTicketManager(ctx, cfgApp)
if err != nil {
log.Printf("Warning: could not create Ticket manager: %v", err)
if !isCompletion {
logger.Warn(ctx, "could not create Ticket manager",
"error", err)
}
ticketMgr = nil
}

Expand Down Expand Up @@ -175,6 +198,21 @@ func initializeApp() (*cli.Command, error) {
Description: translations.GetMessage("app_description", 0, nil),
Commands: commands,
EnableShellCompletion: true,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Usage: translations.GetMessage("flags_global.debug_flag", 0, nil),
},
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: translations.GetMessage("flags_global.verbose_flag", 0, nil),
},
},
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
logger.Initialize(c.Bool("debug"), c.Bool("verbose"))
return ctx, nil
},
}, nil
}

Expand Down
22 changes: 21 additions & 1 deletion internal/ai/cost_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"time"

"github.com/thomas-vilte/matecommit/internal/cache"
Expand Down Expand Up @@ -90,9 +91,17 @@ func (w *CostAwareWrapper) WrapGenerate(

contentHash := w.cache.GenerateHash(providerName + originalModel + prompt)

slog.Debug("checking cache for operation",
"command", command,
"cache_key_hash", contentHash)

if cachedData, hit, err := w.cache.Get(contentHash); err == nil && hit {
var cachedResp interface{}
if err := json.Unmarshal(cachedData, &cachedResp); err == nil {
slog.Info("cache hit",
"command", command,
"cache_key_hash", contentHash)

usage := &models.TokenUsage{
CacheHit: true,
CostUSD: 0,
Expand All @@ -103,6 +112,9 @@ func (w *CostAwareWrapper) WrapGenerate(
}
}

slog.Debug("cache miss, generating new content",
"command", command)

var inputTokens int
tokens, err := w.provider.CountTokens(ctx, prompt)
if err == nil {
Expand Down Expand Up @@ -151,7 +163,15 @@ func (w *CostAwareWrapper) WrapGenerate(
return nil, nil, err
}

_ = w.cache.Set(contentHash, resp)
if err := w.cache.Set(contentHash, resp); err != nil {
slog.Warn("failed to cache response",
"command", command,
"error", err)
} else {
slog.Debug("response cached successfully",
"command", command,
"cache_key_hash", contentHash)
}

if usage != nil {
usage.Model = modelToUse
Expand Down
38 changes: 38 additions & 0 deletions internal/ai/gemini/commit_summarizer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/config"
domainErrors "github.com/thomas-vilte/matecommit/internal/errors"
"github.com/thomas-vilte/matecommit/internal/logger"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ports"
"google.golang.org/genai"
Expand Down Expand Up @@ -89,18 +90,41 @@ func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, onConfir
}

func (s *GeminiCommitSummarizer) defaultGenerate(ctx context.Context, mName string, p string) (interface{}, *models.TokenUsage, error) {
log := logger.FromContext(ctx)

log.Debug("calling gemini API",
"model", mName,
"prompt_length", len(p))

genConfig := GetGenerateConfig(mName, "application/json")

resp, err := s.Client.Models.GenerateContent(ctx, mName, genai.Text(p), genConfig)
if err != nil {
log.Error("gemini API call failed",
"error", err,
"model", mName)
return nil, nil, err
}

usage := extractUsage(resp)

log.Debug("gemini API response received",
"input_tokens", usage.InputTokens,
"output_tokens", usage.OutputTokens,
"candidates", len(resp.Candidates))

return resp, usage, nil
}

func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info models.CommitInfo, count int) ([]models.CommitSuggestion, error) {
log := logger.FromContext(ctx)

log.Info("generating commit suggestions via gemini",
"count", count,
"files", len(info.Files),
"has_issue_info", info.IssueInfo != nil,
"has_ticket_info", info.TicketInfo != nil)

if count <= 0 {
return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "invalid suggestion count", nil)
}
Expand All @@ -111,8 +135,14 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m

prompt := s.generatePrompt(s.config.Language, info, count)

log.Debug("prompt generated",
"prompt_length", len(prompt),
"language", s.config.Language)

resp, usage, err := s.wrapper.WrapGenerate(ctx, "suggest-commits", prompt, s.generateFn)
if err != nil {
log.Error("failed to generate suggestions",
"error", err)
return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error generating content", err)
}

Expand All @@ -137,14 +167,21 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m
return nil, domainErrors.NewAppError(domainErrors.TypeAI, fmt.Sprintf("error parsing AI JSON response (length: %d): %s", respLen, preview), err)
}
if len(suggestions) == 0 {
log.Warn("AI generated no suggestions")
return nil, domainErrors.NewAppError(domainErrors.TypeAI, "AI generated no suggestions", nil)
}
for i := range suggestions {
suggestions[i].Usage = usage
}
if info.IssueInfo != nil && info.IssueInfo.Number > 0 {
log.Debug("ensuring issue reference in suggestions",
"issue_number", info.IssueInfo.Number)
suggestions = s.ensureIssueReference(suggestions, info.IssueInfo.Number)
}

log.Info("commit suggestions generated successfully",
"count", len(suggestions))

return suggestions, nil
}

Expand All @@ -157,6 +194,7 @@ func (s *GeminiCommitSummarizer) parseSuggestionsJSON(responseText string) ([]mo

var jsonSuggestions []CommitSuggestionJSON
if err := json.Unmarshal([]byte(responseText), &jsonSuggestions); err != nil {
// Log at default level (no context available here)
return nil, fmt.Errorf("error parsing JSON: %w", err)
}

Expand Down
26 changes: 25 additions & 1 deletion internal/ai/gemini/issue_content_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
"fmt"
"strings"

"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/config"
domainErrors "github.com/thomas-vilte/matecommit/internal/errors"
"github.com/thomas-vilte/matecommit/internal/logger"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/ai"
"google.golang.org/genai"
)

Expand Down Expand Up @@ -79,10 +80,23 @@ func (s *GeminiIssueContentGenerator) defaultGenerate(ctx context.Context, mName

// GenerateIssueContent generates issue content using Gemini AI.
func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context, request models.IssueGenerationRequest) (*models.IssueGenerationResult, error) {
log := logger.FromContext(ctx)

log.Info("generating issue content via gemini",
"has_diff", request.Diff != "",
"has_description", request.Description != "",
"has_hint", request.Hint != "",
"files_count", len(request.ChangedFiles))

prompt := s.buildIssuePrompt(request)

log.Debug("calling gemini API for issue content",
"prompt_length", len(prompt))

resp, usage, err := s.wrapper.WrapGenerate(ctx, "generate-issue", prompt, s.generateFn)
if err != nil {
log.Error("failed to generate issue content",
"error", err)
return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error generating issue content", err)
}

Expand All @@ -94,16 +108,26 @@ func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context,
}

if responseText == "" {
log.Error("empty response from gemini AI")
return nil, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil)
}

log.Debug("gemini response received",
"response_length", len(responseText))

result, err := s.parseIssueResponse(responseText)
if err != nil {
log.Error("failed to parse issue response",
"error", err)
return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error parsing AI response", err)
}

result.Usage = usage

log.Info("issue content generated successfully via gemini",
"title", result.Title,
"labels_count", len(result.Labels))

return result, nil
}

Expand Down
19 changes: 18 additions & 1 deletion internal/ai/gemini/pull_requests_summarizer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
"fmt"
"strings"

"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/config"
domainErrors "github.com/thomas-vilte/matecommit/internal/errors"
"github.com/thomas-vilte/matecommit/internal/logger"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/ai"
"google.golang.org/genai"
)

Expand Down Expand Up @@ -82,10 +83,20 @@ func (gps *GeminiPRSummarizer) defaultGenerate(ctx context.Context, mName string
}

func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent string) (models.PRSummary, error) {
log := logger.FromContext(ctx)

log.Info("generating PR summary via gemini",
"content_length", len(prContent))

prompt := gps.generatePRPrompt(prContent)

log.Debug("calling gemini API for PR summary",
"prompt_length", len(prompt))

resp, usage, err := gps.wrapper.WrapGenerate(ctx, "summarize-pr", prompt, gps.generateFn)
if err != nil {
log.Error("failed to generate PR summary",
"error", err)
return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, "error generating PR summary", err)
}

Expand All @@ -109,8 +120,14 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent
if respLen > 500 {
preview = responseText[:500] + "..."
}
log.Warn("AI generated no PR title",
"response_length", respLen)
return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, fmt.Sprintf("AI generated no PR title (length: %d): %s", respLen, preview), nil)
}

log.Info("PR summary generated successfully via gemini",
"labels_count", len(jsonSummary.Labels))

return models.PRSummary{
Title: jsonSummary.Title,
Body: jsonSummary.Body,
Expand Down
Loading
Loading