AnalyzeIssue uses the LLM to analyze an AIOps issue and return recommendations.
(ctx context.Context, req *pb.AnalyzeIssueRequest)
| 49 | |
| 50 | // AnalyzeIssue uses the LLM to analyze an AIOps issue and return recommendations. |
| 51 | func (h *Handler) AnalyzeIssue(ctx context.Context, req *pb.AnalyzeIssueRequest) (*pb.AnalyzeIssueResponse, error) { |
| 52 | if req.IssueName == "" { |
| 53 | return nil, status.Errorf(codes.InvalidArgument, "%s", i18n.T("server.analysis.issue_name_required")) |
| 54 | } |
| 55 | |
| 56 | llmClient, err := h.getClient(req.Provider, req.Model, "", nil) |
| 57 | if err != nil { |
| 58 | h.logger.Error(i18n.T("server.analysis.llm_client_failed"), zap.Error(err)) |
| 59 | return nil, status.Errorf(codes.Internal, "%s", i18n.T("server.analysis.get_client_error", err)) |
| 60 | } |
| 61 | |
| 62 | prompt := buildAnalysisPrompt(req) |
| 63 | |
| 64 | // NOTE: Do NOT call enrichPrompt() here. The operator sends its own enriched |
| 65 | // kubernetes_context (up to 30KB with logs, metrics, GitOps, source code, cascade |
| 66 | // analysis) in the RPC request. enrichPrompt() is for interactive CLI sessions only |
| 67 | // and would duplicate/conflict with the operator's context. |
| 68 | response, err := llmClient.SendPrompt(ctx, prompt, nil, 0) |
| 69 | if err != nil { |
| 70 | h.logger.Error(i18n.T("server.analysis.llm_failed"), zap.Error(err), zap.String("issue", req.IssueName)) |
| 71 | return nil, status.Errorf(codes.Internal, "%s", i18n.T("server.analysis.llm_error", err)) |
| 72 | } |
| 73 | |
| 74 | analysis := parseAnalysisResponse(response) |
| 75 | |
| 76 | // GAP-01 fix: detect when the analysis text describes a containment scenario |
| 77 | // ("scale to 0", "stop the bleeding", "containment", "unrecoverable") but the |
| 78 | // first proposed action is a diagnostic. The chaos test (2026-05-23) showed |
| 79 | // this exact divergence: AIInsight said "Scaling to 0 is the only correct |
| 80 | // containment action" while actions[0] was ExecDiagnostic. Logging it as a |
| 81 | // warning preserves the LLM's autonomy while making the regression visible |
| 82 | // in operator logs; reordering happens downstream via the agentic guard. |
| 83 | if firstActionDivergesFromAnalysis(analysis) { |
| 84 | h.logger.Warn(i18n.T("server.analysis.action_diverges_from_analysis"), |
| 85 | zap.String("issue", req.IssueName), |
| 86 | zap.String("analysis_preview", truncate(analysis.Analysis, 200)), |
| 87 | zap.String("first_action", firstActionType(analysis.Actions))) |
| 88 | } |
| 89 | |
| 90 | provider := req.Provider |
| 91 | if provider == "" { |
| 92 | provider = h.defaultProvider |
| 93 | } |
| 94 | |
| 95 | // Map parsed actions to proto SuggestedAction |
| 96 | suggestedActions := make([]*pb.SuggestedAction, 0, len(analysis.Actions)) |
| 97 | for _, a := range analysis.Actions { |
| 98 | suggestedActions = append(suggestedActions, &pb.SuggestedAction{ |
| 99 | Name: a.Name, |
| 100 | Action: a.Action, |
| 101 | Description: a.Description, |
| 102 | Params: a.Params, |
| 103 | }) |
| 104 | } |
| 105 | |
| 106 | return &pb.AnalyzeIssueResponse{ |
| 107 | Analysis: analysis.Analysis, |
| 108 | Confidence: analysis.Confidence, |
nothing calls this directly
no test coverage detected