package handlers import ( "context" "encoding/json" "fmt" "net/http" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ExportFMEA handles GET /projects/:id/fmea/export // Returns an xlsx file in VDA FMEA format. func (h *IACEHandler) ExportFMEA(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } ctx := c.Request.Context() project, err := h.store.GetProject(ctx, projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Load components components, _ := h.store.ListComponents(ctx, projectID) // Load all failure modes allFMs := iace.GetFailureModeLibrary() // Build FMEA rows: each component × matching FMs var rows []iace.FMEAExportRow for _, comp := range components { compType := string(comp.ComponentType) var compFMs []iace.FailureModeEntry for _, fm := range allFMs { if fm.ComponentType == compType { compFMs = append(compFMs, fm) } } if len(compFMs) == 0 { // Fallback: mechanical FMs for _, fm := range allFMs { if fm.ComponentType == "mechanical" && len(compFMs) < 3 { compFMs = append(compFMs, fm) } } } for _, fm := range compFMs { s, o, d := fm.DefaultSeverity, fm.DefaultOccurrence, fm.DefaultDetection rows = append(rows, iace.FMEAExportRow{ ComponentName: comp.Name, ComponentType: compType, FailureMode: fm.NameDE, FailureEffect: fm.Effect, FailureCause: fm.DetectionHint, Severity: s, Occurrence: o, Detection: d, RPZ: s * o * d, AP: iace.CalculateAP(s, o, d), Measure: "", DetectionHint: fm.DetectionHint, }) } } xlsxBytes, err := iace.GenerateFMEAExcel(project.MachineName, rows) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel generation failed: %v", err)}) return } filename := fmt.Sprintf("FMEA-%s.xlsx", project.MachineName) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xlsxBytes) } // SuggestFailureModes handles POST /projects/:id/components/:cid/suggest-fms // Uses LLM to suggest failure modes for a specific component. func (h *IACEHandler) SuggestFailureModes(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } componentID, err := uuid.Parse(c.Param("cid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"}) return } ctx := c.Request.Context() project, err := h.store.GetProject(ctx, projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } comp, err := h.store.GetComponent(ctx, componentID) if err != nil || comp == nil { c.JSON(http.StatusNotFound, gin.H{"error": "component not found"}) return } // Build LLM prompt prompt := fmt.Sprintf( `Du bist ein FMEA-Experte (Fehlermoeglich- und Einflussanalyse) nach AIAG-VDA. Fuer die Komponente "%s" (Typ: %s) in der Maschine "%s" (%s): Nenne die 5 wichtigsten Failure Modes. Fuer jeden: - mode: Kurzbezeichnung der Fehlerart - name_de: Deutsche Beschreibung - effect: Systemauswirkung - severity: Schwere 1-10 (10=katastrophal) - occurrence: Auftretenswahrscheinlichkeit 1-10 (10=sehr haeufig) - detection: Entdeckbarkeit 1-10 (10=nicht erkennbar) Antworte NUR mit einem JSON-Array, keine Erklaerungen: [{"mode":"...","name_de":"...","effect":"...","severity":N,"occurrence":N,"detection":N}]`, comp.Name, comp.ComponentType, project.MachineName, project.MachineType) // Try LLM suggestions, err := callLLMForFMs(ctx, h.llmRegistry, prompt) if err != nil { // Fallback: return library FMs for this component type allFMs := iace.GetFailureModeLibrary() var fallback []iace.FailureModeEntry for _, fm := range allFMs { if fm.ComponentType == string(comp.ComponentType) && len(fallback) < 5 { fallback = append(fallback, fm) } } c.JSON(http.StatusOK, gin.H{ "suggestions": fallback, "source": "library_fallback", "total": len(fallback), }) return } c.JSON(http.StatusOK, gin.H{ "suggestions": suggestions, "source": "llm", "total": len(suggestions), }) } func callLLMForFMs(ctx context.Context, registry *llm.ProviderRegistry, prompt string) ([]iace.FailureModeEntry, error) { if registry == nil { return nil, fmt.Errorf("no LLM registry") } provider, err := registry.GetAvailable(ctx) if err != nil { return nil, fmt.Errorf("no LLM provider available: %w", err) } resp, err := provider.Chat(ctx, &llm.ChatRequest{ Messages: []llm.Message{ {Role: "user", Content: prompt}, }, Temperature: 0.3, MaxTokens: 1000, }) if err != nil { return nil, fmt.Errorf("LLM call failed: %w", err) } // Parse JSON from response content := strings.TrimSpace(resp.Message.Content) // Strip markdown code fences if present content = strings.TrimPrefix(content, "```json") content = strings.TrimPrefix(content, "```") content = strings.TrimSuffix(content, "```") content = strings.TrimSpace(content) var rawFMs []struct { Mode string `json:"mode"` NameDE string `json:"name_de"` Effect string `json:"effect"` Severity int `json:"severity"` Occurrence int `json:"occurrence"` Detection int `json:"detection"` } if err := json.Unmarshal([]byte(content), &rawFMs); err != nil { return nil, fmt.Errorf("failed to parse LLM response: %w", err) } var result []iace.FailureModeEntry for i, fm := range rawFMs { result = append(result, iace.FailureModeEntry{ ID: fmt.Sprintf("LLM-%03d", i+1), ComponentType: "llm_suggested", Mode: fm.Mode, NameDE: fm.NameDE, Effect: fm.Effect, DefaultSeverity: clamp(fm.Severity, 1, 10), DefaultOccurrence: clamp(fm.Occurrence, 1, 10), DefaultDetection: clamp(fm.Detection, 1, 10), }) } return result, nil } func clamp(v, min, max int) int { if v < min { return min } if v > max { return max } return v }