feat: AI-анализ terraform plan (уровни 0-5, Google Gemini)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user