Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/iace_handler_fmea.go
T
Benjamin Admin 7d9f5a1f76
Build + Deploy / build-admin-compliance (push) Successful in 1m42s
Build + Deploy / build-backend-compliance (push) Successful in 15s
Build + Deploy / build-ai-sdk (push) Successful in 9s
Build + Deploy / build-developer-portal (push) Successful in 11s
Build + Deploy / build-tts (push) Successful in 18s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 14s
Build + Deploy / build-dsms-node (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m32s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 41s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 21s
CI / validate-canonical-controls (push) Successful in 13s
Build + Deploy / trigger-orca (push) Successful in 2m25s
feat(iace): LLM-gestuetzte Failure Mode Erkennung
POST /projects/:id/components/:cid/suggest-fms
- Baut FMEA-Experten-Prompt aus Komponentenname + Maschinenkontext
- LLM antwortet mit 5 FMs als JSON (Mode, Effect, S/O/D)
- Fallback auf Bibliotheks-FMs wenn LLM nicht verfuegbar
- Nutzt ProviderRegistry (Ollama primary, Anthropic fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:52:16 +02:00

223 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}