From 5a83c15096fa5aaec6e5c9c0ae382faa62bf32b3 Mon Sep 17 00:00:00 2001 From: Aashita101 Date: Tue, 9 Jun 2026 19:15:36 +0530 Subject: [PATCH 1/4] fix(ai): wire MaxTokens and Temperature through AnalyzerConfig Signed-off-by: Aashita101 --- internal/ai/analyzer.go | 128 ++++++++++------------------------------ 1 file changed, 31 insertions(+), 97 deletions(-) diff --git a/internal/ai/analyzer.go b/internal/ai/analyzer.go index 6d520c2..f977c4c 100644 --- a/internal/ai/analyzer.go +++ b/internal/ai/analyzer.go @@ -16,18 +16,22 @@ import ( // DefaultAnalyzer implements doctor.Analyzer by sending findings to an LLM // provider and parsing the structured response. type DefaultAnalyzer struct { - provider Provider - cache *Cache - privacy PrivacyMode - logger *slog.Logger + provider Provider + cache *Cache + privacy PrivacyMode + logger *slog.Logger + maxTokens int + temperature float64 } // AnalyzerConfig holds configuration for constructing a DefaultAnalyzer. type AnalyzerConfig struct { - Provider Provider - Cache *Cache - Privacy PrivacyMode - Logger *slog.Logger + Provider Provider + Cache *Cache + Privacy PrivacyMode + Logger *slog.Logger + MaxTokens int + Temperature float64 } // NewAnalyzer creates a DefaultAnalyzer. @@ -41,10 +45,12 @@ func NewAnalyzer(cfg AnalyzerConfig) *DefaultAnalyzer { logger = slog.New(slog.NewTextHandler(io.Discard, nil)) } return &DefaultAnalyzer{ - provider: cfg.Provider, - cache: cfg.Cache, - privacy: privacy, - logger: logger, + provider: cfg.Provider, + cache: cfg.Cache, + privacy: privacy, + logger: logger, + maxTokens: cfg.MaxTokens, + temperature: cfg.Temperature, } } @@ -66,103 +72,31 @@ func (a *DefaultAnalyzer) Analyze(ctx context.Context, req doctor.AnalysisReques a.logger.Debug("sending to AI provider", "provider", a.provider.Name(), "privacy", a.privacy, - "prompt_len", len(userPrompt), + "findings", len(req.Findings), ) - // Call the LLM. - completion, err := a.provider.Complete(ctx, CompletionRequest{ + resp, err := a.provider.Complete(ctx, CompletionRequest{ SystemPrompt: SystemPrompt, UserPrompt: userPrompt, + MaxTokens: a.maxTokens, + Temperature: a.temperature, }) if err != nil { - return nil, fmt.Errorf("AI provider %s: %w", a.provider.Name(), err) + return nil, fmt.Errorf("AI provider error: %w", err) } - a.logger.Info("AI analysis complete", - "provider", a.provider.Name(), - "model", completion.Model, - "tokens", completion.TokensUsed, - ) - - // Parse the structured JSON response. - response, err := parseAnalysisResponse(completion.Text) - if err != nil { - // If JSON parsing fails, treat the entire response as a summary. - a.logger.Warn("AI response was not valid JSON, using as plain text summary", "error", err) - response = &doctor.AnalysisResponse{ - Summary: completion.Text, - } + var result doctor.AnalysisResponse + if err := json.Unmarshal([]byte(resp.Text), &result); err != nil { + return nil, fmt.Errorf("parsing AI response: %w", err) } - response.TokensUsed = completion.TokensUsed + result.TokensUsed = resp.TokensUsed + result.Model = resp.Model - // Cache the result. if a.cache != nil { fingerprint := findingsFingerprint(req.Findings) - a.cache.Set(fingerprint, response) + a.cache.Set(fingerprint, &result) } - return response, nil -} - -// parseAnalysisResponse attempts to parse the LLM response as structured JSON. -func parseAnalysisResponse(text string) (*doctor.AnalysisResponse, error) { - // Try to extract JSON from the response (LLMs sometimes wrap in markdown). - jsonText := extractJSON(text) - - var resp doctor.AnalysisResponse - if err := json.Unmarshal([]byte(jsonText), &resp); err != nil { - return nil, fmt.Errorf("parsing AI response JSON: %w", err) - } - return &resp, nil -} - -// extractJSON tries to find a JSON object in the text, handling markdown code blocks. -func extractJSON(text string) string { - // Look for ```json ... ``` blocks. - if start := findSubstring(text, "```json"); start >= 0 { - content := text[start+7:] - if end := findSubstring(content, "```"); end >= 0 { - return content[:end] - } - } - // Look for ``` ... ``` blocks. - if start := findSubstring(text, "```"); start >= 0 { - content := text[start+3:] - if end := findSubstring(content, "```"); end >= 0 { - return content[:end] - } - } - // Try the raw text as-is. - return text -} - -func findSubstring(s, substr string) int { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return i - } - } - return -1 -} - -// findingsFingerprint creates a cache key from findings. -// It uses rule names and severities, not exact values, so similar -// situations share cache entries. -func findingsFingerprint(findings []doctor.Finding) string { - if len(findings) == 0 { - return "healthy" - } - parts := make([]string, len(findings)) - for i, f := range findings { - parts[i] = fmt.Sprintf("%s:%s", f.Rule, f.Severity) - } - // Simple concatenation — good enough for cache key. - result := "" - for i, p := range parts { - if i > 0 { - result += "|" - } - result += p - } - return result + return &result, nil } + From 0264ad16afbcfe53d76a36dbf5a9c5a8502dc6c0 Mon Sep 17 00:00:00 2001 From: Aashita101 Date: Tue, 9 Jun 2026 19:29:04 +0530 Subject: [PATCH 2/4] fix(ai): pass MaxTokens and Temperature into AnalyzerConfig Signed-off-by: Aashita101 --- internal/cli/doctor.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 5076c11..8489122 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -452,12 +452,15 @@ func buildAnalyzer(c *config.Config, logger *slog.Logger) (doctor.Analyzer, erro privacy = ai.PrivacySummary } - return ai.NewAnalyzer(ai.AnalyzerConfig{ - Provider: provider, - Cache: cache, - Privacy: privacy, - Logger: logger, +return ai.NewAnalyzer(ai.AnalyzerConfig{ + Provider: provider, + Cache: cache, + Privacy: privacy, + Logger: logger, + MaxTokens: aiCfg.MaxTokens, + Temperature: aiCfg.Temperature, }), nil + } func runDiagnosticCycle( From 5eff625c0ed531ae0d15a9d703b7159dd2683b0f Mon Sep 17 00:00:00 2001 From: Aashita101 Date: Tue, 9 Jun 2026 20:02:23 +0530 Subject: [PATCH 3/4] fix(ai): wire MaxTokens and Temperature through AnalyzerConfig Signed-off-by: Aashita101 --- internal/ai/analyzer.go | 88 +++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/internal/ai/analyzer.go b/internal/ai/analyzer.go index f977c4c..07f2058 100644 --- a/internal/ai/analyzer.go +++ b/internal/ai/analyzer.go @@ -72,31 +72,97 @@ func (a *DefaultAnalyzer) Analyze(ctx context.Context, req doctor.AnalysisReques a.logger.Debug("sending to AI provider", "provider", a.provider.Name(), "privacy", a.privacy, - "findings", len(req.Findings), + "prompt_len", len(userPrompt), ) - resp, err := a.provider.Complete(ctx, CompletionRequest{ + // Call the LLM. + completion, err := a.provider.Complete(ctx, CompletionRequest{ SystemPrompt: SystemPrompt, UserPrompt: userPrompt, MaxTokens: a.maxTokens, Temperature: a.temperature, }) if err != nil { - return nil, fmt.Errorf("AI provider error: %w", err) + return nil, fmt.Errorf("AI provider %s: %w", a.provider.Name(), err) } - var result doctor.AnalysisResponse - if err := json.Unmarshal([]byte(resp.Text), &result); err != nil { - return nil, fmt.Errorf("parsing AI response: %w", err) + a.logger.Info("AI analysis complete", + "provider", a.provider.Name(), + "model", completion.Model, + "tokens", completion.TokensUsed, + ) + + // Parse the structured JSON response. + response, err := parseAnalysisResponse(completion.Text) + if err != nil { + // If JSON parsing fails, treat the entire response as a summary. + a.logger.Warn("AI response was not valid JSON, using as plain text summary", "error", err) + response = &doctor.AnalysisResponse{ + Summary: completion.Text, + } } - result.TokensUsed = resp.TokensUsed - result.Model = resp.Model + response.TokensUsed = completion.TokensUsed + // Cache the result. if a.cache != nil { fingerprint := findingsFingerprint(req.Findings) - a.cache.Set(fingerprint, &result) + a.cache.Set(fingerprint, response) + } + + return response, nil +} + +// parseAnalysisResponse attempts to parse the LLM response as structured JSON. +func parseAnalysisResponse(text string) (*doctor.AnalysisResponse, error) { + jsonText := extractJSON(text) + var resp doctor.AnalysisResponse + if err := json.Unmarshal([]byte(jsonText), &resp); err != nil { + return nil, fmt.Errorf("parsing AI response JSON: %w", err) + } + return &resp, nil +} + +// extractJSON tries to find a JSON object in the text, handling markdown code blocks. +func extractJSON(text string) string { + if start := findSubstring(text, "```json"); start >= 0 { + content := text[start+7:] + if end := findSubstring(content, "```"); end >= 0 { + return content[:end] + } + } + if start := findSubstring(text, "```"); start >= 0 { + content := text[start+3:] + if end := findSubstring(content, "```"); end >= 0 { + return content[:end] + } + } + return text +} + +func findSubstring(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } } + return -1 +} - return &result, nil +// findingsFingerprint creates a cache key from findings. +func findingsFingerprint(findings []doctor.Finding) string { + if len(findings) == 0 { + return "healthy" + } + parts := make([]string, len(findings)) + for i, f := range findings { + parts[i] = fmt.Sprintf("%s:%s", f.Rule, f.Severity) + } + result := "" + for i, p := range parts { + if i > 0 { + result += "|" + } + result += p + } + return result } - From 326aa386e7e0aaa17b7348491efe4a245828237b Mon Sep 17 00:00:00 2001 From: Aashita101 Date: Tue, 9 Jun 2026 20:17:03 +0530 Subject: [PATCH 4/4] style: fix gofmt formatting in buildAnalyzer Signed-off-by: Aashita101 --- internal/cli/doctor.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 8489122..7965c3b 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -452,7 +452,7 @@ func buildAnalyzer(c *config.Config, logger *slog.Logger) (doctor.Analyzer, erro privacy = ai.PrivacySummary } -return ai.NewAnalyzer(ai.AnalyzerConfig{ + return ai.NewAnalyzer(ai.AnalyzerConfig{ Provider: provider, Cache: cache, Privacy: privacy, @@ -460,7 +460,6 @@ return ai.NewAnalyzer(ai.AnalyzerConfig{ MaxTokens: aiCfg.MaxTokens, Temperature: aiCfg.Temperature, }), nil - } func runDiagnosticCycle(