// 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)) }