Files
sless/internal/ai/analyzer.go
T

86 lines
3.1 KiB
Go

// internal/ai/analyzer.go
// Дата: 2026-03-12
// Оркестратор AI-анализа: читает уровень, формирует промпт,
// вызывает LLM-провайдер, форматирует вывод для пользователя.
package ai
import (
"context"
"fmt"
"strings"
)
// Analyzer — основной компонент AI-анализа.
// Создаётся один раз при старте sless-plan, используется для одного анализа.
type Analyzer struct {
provider LLMProvider
level int
}
// NewAnalyzer создаёт Analyzer из конфига.
// Возвращает ошибку если провайдер не может быть инициализирован.
func NewAnalyzer(cfg Config) (*Analyzer, error) {
if cfg.HintLevel == 0 {
// Уровень 0 — AI выключен, провайдер не нужен
return &Analyzer{level: 0}, nil
}
provider, err := NewProvider(cfg)
if err != nil {
return nil, fmt.Errorf("ошибка инициализации AI провайдера: %w", err)
}
return &Analyzer{
provider: provider,
level: cfg.HintLevel,
}, nil
}
// IsEnabled возвращает true если уровень > 0 и провайдер инициализирован.
func (a *Analyzer) IsEnabled() bool {
return a.level > 0 && a.provider != nil
}
// Analyze отправляет вывод terraform plan в LLM и возвращает отформатированный результат.
// planText — текстовый вывод terraform plan.
func (a *Analyzer) Analyze(ctx context.Context, planText string) (string, error) {
if !a.IsEnabled() {
return "", nil
}
// Ограничиваем размер payload — большие планы обрезаем
// чтобы не превысить лимит токенов LLM и не отправлять лишнее
planText = truncatePlan(planText, 8000)
prompt := BuildPrompt(a.level, planText)
result, err := a.provider.Analyze(ctx, prompt)
if err != nil {
return "", fmt.Errorf("AI анализ не удался: %w", err)
}
return FormatResult(a.level, a.provider.Name(), result), nil
}
// FormatResult оборачивает ответ LLM в читаемый блок для вывода в терминал.
func FormatResult(level int, providerName, result string) string {
separator := strings.Repeat("─", 60)
header := fmt.Sprintf("AI Analysis (level %d: %s) via %s", level, LevelName(level), providerName)
return fmt.Sprintf("\n%s\n %s\n%s\n%s\n%s\n",
separator,
header,
separator,
result,
separator,
)
}
// truncatePlan обрезает текст плана до maxChars символов с пометкой об обрезке.
// Нужно чтобы не превышать лимиты токенов LLM и не отправлять лишние данные.
func truncatePlan(text string, maxChars int) string {
if len(text) <= maxChars {
return text
}
return text[:maxChars] + fmt.Sprintf("\n\n[... обрезано, показаны первые %d символов из %d ...]", maxChars, len(text))
}