Skip to content

Commit b75ec94

Browse files
authored
Merge pull request #127 from aqsaaqeel/feat/generate-stats
feat: generate stats for llms used
2 parents 609e976 + 287d3ef commit b75ec94

File tree

6 files changed

+618
-5
lines changed

6 files changed

+618
-5
lines changed

cmd/cli/createMsg.go

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,25 +354,74 @@ func generateMessage(ctx context.Context, provider llm.Provider, changes string,
354354

355355
// generateMessageWithCache generates a commit message with caching support.
356356
func generateMessageWithCache(ctx context.Context, provider llm.Provider, store *store.StoreMethods, providerType types.LLMProvider, changes string, opts *types.GenerationOptions) (string, error) {
357+
startTime := time.Now()
358+
359+
// Determine if this is a first attempt (cache check eligible)
360+
isFirstAttempt := opts == nil || opts.Attempt <= 1
361+
357362
// Check cache first (only for first attempt to avoid caching regenerations)
358-
if opts == nil || opts.Attempt <= 1 {
363+
if isFirstAttempt {
359364
if cachedEntry, found := store.GetCachedMessage(providerType, changes, opts); found {
360365
pterm.Info.Printf("Using cached commit message (saved $%.4f)\n", cachedEntry.Cost)
366+
367+
// Record cache hit event
368+
event := &types.GenerationEvent{
369+
Provider: providerType,
370+
Success: true,
371+
GenerationTime: float64(time.Since(startTime).Nanoseconds()) / 1e6, // Convert to milliseconds
372+
TokensUsed: 0, // No tokens used for cached result
373+
Cost: 0, // No cost for cached result
374+
CacheHit: true,
375+
CacheChecked: true,
376+
Timestamp: time.Now().UTC().Format(time.RFC3339),
377+
}
378+
379+
if err := store.RecordGenerationEvent(event); err != nil {
380+
// Log the error but don't fail the operation
381+
fmt.Printf("Warning: Failed to record usage statistics: %v\n", err)
382+
}
383+
361384
return cachedEntry.Message, nil
362385
}
363386
}
364387

365388
// Generate new message
366389
message, err := provider.Generate(ctx, changes, opts)
390+
generationTime := float64(time.Since(startTime).Nanoseconds()) / 1e6 // Convert to milliseconds
391+
392+
// Estimate tokens and cost
393+
inputTokens := estimateTokens(types.BuildCommitPrompt(changes, opts))
394+
outputTokens := 100 // Estimate output tokens
395+
cost := estimateCost(providerType, inputTokens, outputTokens)
396+
397+
// Record generation event
398+
event := &types.GenerationEvent{
399+
Provider: providerType,
400+
Success: err == nil,
401+
GenerationTime: generationTime,
402+
TokensUsed: inputTokens + outputTokens,
403+
Cost: cost,
404+
CacheHit: false,
405+
CacheChecked: isFirstAttempt, // Only first attempts check cache
406+
Timestamp: time.Now().UTC().Format(time.RFC3339),
407+
}
408+
409+
if err != nil {
410+
event.ErrorMessage = err.Error()
411+
}
412+
413+
// Record the event regardless of success/failure
414+
if statsErr := store.RecordGenerationEvent(event); statsErr != nil {
415+
// Log the error but don't fail the operation
416+
fmt.Printf("Warning: Failed to record usage statistics: %v\n", statsErr)
417+
}
418+
367419
if err != nil {
368420
return "", err
369421
}
370422

371423
// Cache the result (only for first attempt)
372-
if opts == nil || opts.Attempt <= 1 {
373-
// Estimate cost for caching
374-
cost := estimateCost(providerType, estimateTokens(types.BuildCommitPrompt(changes, opts)), 100)
375-
424+
if isFirstAttempt {
376425
// Store in cache
377426
if cacheErr := store.SetCachedMessage(providerType, changes, opts, message, cost, nil); cacheErr != nil {
378427
// Log cache error but don't fail the generation

cmd/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func init() {
141141
rootCmd.AddCommand(creatCommitMsg)
142142
rootCmd.AddCommand(llmCmd)
143143
rootCmd.AddCommand(cacheCmd)
144+
rootCmd.AddCommand(statsCmd)
144145
llmCmd.AddCommand(llmSetupCmd)
145146
llmCmd.AddCommand(llmUpdateCmd)
146147
cacheCmd.AddCommand(cacheStatsCmd)

cmd/cli/stats.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"time"
7+
8+
"github.com/dfanso/commit-msg/cmd/cli/store"
9+
"github.com/dfanso/commit-msg/pkg/types"
10+
"github.com/pterm/pterm"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// statsCmd represents the statistics command
15+
var statsCmd = &cobra.Command{
16+
Use: "stats",
17+
Short: "Display usage statistics",
18+
Long: `Display comprehensive usage statistics including:
19+
- Most used LLM provider
20+
- Average generation time
21+
- Success/failure rates
22+
- Token usage per provider
23+
- Cache hit rates
24+
- Cost tracking`,
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
Store, err := store.NewStoreMethods()
27+
if err != nil {
28+
return fmt.Errorf("failed to initialize store: %w", err)
29+
}
30+
31+
reset, _ := cmd.Flags().GetBool("reset")
32+
if reset {
33+
if err := resetStatistics(Store); err != nil {
34+
return err
35+
}
36+
return nil
37+
}
38+
39+
detailed, _ := cmd.Flags().GetBool("detailed")
40+
return displayStatistics(Store, detailed)
41+
},
42+
}
43+
44+
func init() {
45+
statsCmd.Flags().Bool("detailed", false, "Show detailed per-provider statistics")
46+
statsCmd.Flags().Bool("reset", false, "Reset all usage statistics")
47+
}
48+
49+
func displayStatistics(store *store.StoreMethods, detailed bool) error {
50+
stats := store.GetUsageStats()
51+
52+
if stats.TotalGenerations == 0 {
53+
pterm.Info.Println("No usage statistics available yet.")
54+
pterm.Info.Println("Statistics will be collected as you use the commit message generator.")
55+
return nil
56+
}
57+
58+
// Header
59+
pterm.DefaultHeader.WithFullWidth().
60+
WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).
61+
WithTextStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)).
62+
Println("Usage Statistics")
63+
64+
pterm.Println()
65+
66+
// Overall Statistics
67+
pterm.DefaultSection.WithLevel(2).Println("Overall Statistics")
68+
69+
overallData := [][]string{
70+
{"Total Generations", fmt.Sprintf("%d", stats.TotalGenerations)},
71+
{"Successful Generations", fmt.Sprintf("%d (%.1f%%)", stats.SuccessfulGenerations, store.GetOverallSuccessRate())},
72+
{"Failed Generations", fmt.Sprintf("%d (%.1f%%)", stats.FailedGenerations, float64(stats.FailedGenerations)/float64(stats.TotalGenerations)*100)},
73+
{"Average Generation Time", fmt.Sprintf("%.1f ms", stats.AverageGenerationTime)},
74+
{"Total Cost", fmt.Sprintf("$%.4f", stats.TotalCost)},
75+
{"Total Tokens Used", fmt.Sprintf("%d", stats.TotalTokensUsed)},
76+
}
77+
78+
if stats.CacheHits > 0 || stats.CacheMisses > 0 {
79+
cacheRate := store.GetCacheHitRate()
80+
overallData = append(overallData, []string{"Cache Hit Rate", fmt.Sprintf("%.1f%% (%d hits, %d misses)", cacheRate, stats.CacheHits, stats.CacheMisses)})
81+
}
82+
83+
if stats.FirstUse != "" {
84+
if firstUse, err := time.Parse(time.RFC3339, stats.FirstUse); err == nil {
85+
overallData = append(overallData, []string{"First Use", firstUse.Local().Format("Jan 2, 2006 15:04")})
86+
}
87+
}
88+
89+
if stats.LastUse != "" {
90+
if lastUse, err := time.Parse(time.RFC3339, stats.LastUse); err == nil {
91+
overallData = append(overallData, []string{"Last Use", lastUse.Local().Format("Jan 2, 2006 15:04")})
92+
}
93+
}
94+
95+
pterm.DefaultTable.WithHasHeader(false).WithData(overallData).Render()
96+
pterm.Println()
97+
98+
// Provider Rankings
99+
if len(stats.ProviderStats) > 0 {
100+
pterm.DefaultSection.WithLevel(2).Println("Provider Rankings")
101+
102+
ranking := store.GetProviderRanking()
103+
rankingData := [][]string{{"Rank", "Provider", "Uses", "Success Rate", "Avg Time (ms)", "Total Cost"}}
104+
105+
for i, provider := range ranking {
106+
providerStats := stats.ProviderStats[provider]
107+
rankingData = append(rankingData, []string{
108+
fmt.Sprintf("#%d", i+1),
109+
string(provider),
110+
fmt.Sprintf("%d", providerStats.TotalUses),
111+
fmt.Sprintf("%.1f%%", providerStats.SuccessRate),
112+
fmt.Sprintf("%.1f", providerStats.AverageGenerationTime),
113+
fmt.Sprintf("$%.4f", providerStats.TotalCost),
114+
})
115+
}
116+
117+
pterm.DefaultTable.WithHasHeader(true).WithData(rankingData).Render()
118+
pterm.Println()
119+
}
120+
121+
// Detailed Provider Statistics
122+
if detailed && len(stats.ProviderStats) > 0 {
123+
pterm.DefaultSection.WithLevel(2).Println("Detailed Provider Statistics")
124+
125+
// Sort providers alphabetically for consistent display
126+
var providers []types.LLMProvider
127+
for provider := range stats.ProviderStats {
128+
providers = append(providers, provider)
129+
}
130+
sort.Slice(providers, func(i, j int) bool {
131+
return string(providers[i]) < string(providers[j])
132+
})
133+
134+
for _, provider := range providers {
135+
providerStats := stats.ProviderStats[provider]
136+
137+
pterm.DefaultSection.WithLevel(3).Printf("%s Details", provider)
138+
139+
providerData := [][]string{
140+
{"Total Uses", fmt.Sprintf("%d", providerStats.TotalUses)},
141+
{"Successful Uses", fmt.Sprintf("%d", providerStats.SuccessfulUses)},
142+
{"Failed Uses", fmt.Sprintf("%d", providerStats.FailedUses)},
143+
{"Success Rate", fmt.Sprintf("%.1f%%", providerStats.SuccessRate)},
144+
{"Average Generation Time", fmt.Sprintf("%.1f ms", providerStats.AverageGenerationTime)},
145+
{"Total Cost", fmt.Sprintf("$%.4f", providerStats.TotalCost)},
146+
{"Total Tokens Used", fmt.Sprintf("%d", providerStats.TotalTokensUsed)},
147+
}
148+
149+
if providerStats.FirstUsed != "" {
150+
if firstUsed, err := time.Parse(time.RFC3339, providerStats.FirstUsed); err == nil {
151+
providerData = append(providerData, []string{"First Used", firstUsed.Local().Format("Jan 2, 2006 15:04")})
152+
}
153+
}
154+
155+
if providerStats.LastUsed != "" {
156+
if lastUsed, err := time.Parse(time.RFC3339, providerStats.LastUsed); err == nil {
157+
providerData = append(providerData, []string{"Last Used", lastUsed.Local().Format("Jan 2, 2006 15:04")})
158+
}
159+
}
160+
161+
pterm.DefaultTable.WithHasHeader(false).WithData(providerData).Render()
162+
pterm.Println()
163+
}
164+
}
165+
166+
// Show tips
167+
pterm.DefaultSection.WithLevel(2).Println("Tips")
168+
pterm.Info.Println("• Use --detailed flag to see comprehensive per-provider statistics")
169+
pterm.Info.Println("• Statistics help identify your most reliable and cost-effective providers")
170+
pterm.Info.Println("• Cache hits save both time and API costs")
171+
pterm.Info.Println("• Use --reset flag to clear all statistics (irreversible)")
172+
173+
return nil
174+
}
175+
176+
func resetStatistics(store *store.StoreMethods) error {
177+
pterm.Warning.Println("This will permanently delete all usage statistics.")
178+
179+
confirm, _ := pterm.DefaultInteractiveConfirm.
180+
WithDefaultValue(false).
181+
WithDefaultText("Are you sure you want to reset all statistics?").
182+
Show()
183+
184+
if !confirm {
185+
pterm.Info.Println("Statistics reset cancelled.")
186+
return nil
187+
}
188+
189+
if err := store.ResetUsageStats(); err != nil {
190+
return fmt.Errorf("failed to reset statistics: %w", err)
191+
}
192+
193+
pterm.Success.Println("All usage statistics have been reset.")
194+
return nil
195+
}

cmd/cli/store/store.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import (
1111
"github.com/99designs/keyring"
1212

1313
"github.com/dfanso/commit-msg/internal/cache"
14+
"github.com/dfanso/commit-msg/internal/usage"
1415
"github.com/dfanso/commit-msg/pkg/types"
1516
StoreUtils "github.com/dfanso/commit-msg/utils"
1617
)
1718

1819
type StoreMethods struct {
1920
ring keyring.Keyring
2021
cache *cache.CacheManager
22+
usage *usage.StatsManager
2123
}
2224

2325
// NewStoreMethods creates a new StoreMethods instance with cache support.
@@ -34,9 +36,15 @@ func NewStoreMethods() (*StoreMethods, error) {
3436
return nil, fmt.Errorf("failed to initialize cache: %w", err)
3537
}
3638

39+
usageManager, err := usage.NewStatsManager()
40+
if err != nil {
41+
return nil, fmt.Errorf("failed to initialize usage statistics: %w", err)
42+
}
43+
3744
return &StoreMethods{
3845
ring: ring,
3946
cache: cacheManager,
47+
usage: usageManager,
4048
}, nil
4149
}
4250

@@ -409,3 +417,45 @@ func (s *StoreMethods) GetCacheStats() *types.CacheStats {
409417
func (s *StoreMethods) CleanupCache() error {
410418
return s.cache.Cleanup()
411419
}
420+
421+
// Usage statistics management methods
422+
423+
// GetUsageManager returns the usage statistics manager instance.
424+
func (s *StoreMethods) GetUsageManager() *usage.StatsManager {
425+
return s.usage
426+
}
427+
428+
// RecordGenerationEvent records a commit message generation event for statistics.
429+
func (s *StoreMethods) RecordGenerationEvent(event *types.GenerationEvent) error {
430+
return s.usage.RecordGeneration(event)
431+
}
432+
433+
// GetUsageStats returns comprehensive usage statistics.
434+
func (s *StoreMethods) GetUsageStats() *types.UsageStats {
435+
return s.usage.GetStats()
436+
}
437+
438+
// GetMostUsedProvider returns the most frequently used LLM provider.
439+
func (s *StoreMethods) GetMostUsedProvider() (types.LLMProvider, int) {
440+
return s.usage.GetMostUsedProvider()
441+
}
442+
443+
// GetOverallSuccessRate returns the overall success rate as a percentage.
444+
func (s *StoreMethods) GetOverallSuccessRate() float64 {
445+
return s.usage.GetSuccessRate()
446+
}
447+
448+
// GetCacheHitRate returns the cache hit rate as a percentage.
449+
func (s *StoreMethods) GetCacheHitRate() float64 {
450+
return s.usage.GetCacheHitRate()
451+
}
452+
453+
// GetProviderRanking returns providers ranked by usage frequency.
454+
func (s *StoreMethods) GetProviderRanking() []types.LLMProvider {
455+
return s.usage.GetProviderRanking()
456+
}
457+
458+
// ResetUsageStats clears all usage statistics.
459+
func (s *StoreMethods) ResetUsageStats() error {
460+
return s.usage.ResetStats()
461+
}

0 commit comments

Comments
 (0)