From 0371eecc037a6fac431a3df16be0a6eaa6dd53ba Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 9 May 2026 08:30:45 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Struktureller=20Fix=20=E2=80=94=20Maschi?= =?UTF-8?q?nentyp-Filter=20fuer=20Keywords=20+=20Patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: Cobot-Projekt hatte 52 Pressen-Hazards weil Keywords wie "stempel" und "stoessel" ohne Maschinentyp-Kontext matchten. FIX an 3 Stellen: 1. KeywordEntry.MachineTypes — Pressen-Keywords nur fuer press/*_press 2. ParseNarrative(text, machineType) — Parser laedt Maschinentyp aus Projekt 3. HazardPattern.MachineTypes — Pressen-Patterns (HP045-HP058) nur fuer Pressen Verhindert zukuenftig falsche Zuordnungen bei neuen Kundenprojekten. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/handlers/iace_handler_parser.go | 13 +++++++-- .../internal/iace/hazard_pattern_types.go | 5 ++++ .../internal/iace/hazard_patterns_press.go | 28 +++++++++---------- .../internal/iace/keyword_dictionary.go | 18 +++++++----- .../internal/iace/narrative_parser.go | 24 +++++++++++++++- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go index dce66d0..5600b3e 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go @@ -6,6 +6,7 @@ import ( "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // ParseNarrativeRequest is the request body for POST /projects/:id/parse-narrative. @@ -43,8 +44,16 @@ func (h *IACEHandler) ParseNarrative(c *gin.Context) { return } - // 1. Parse narrative text deterministically - parseResult := iace.ParseNarrative(req.NarrativeText) + // Load project to get machine type for context-aware parsing + var machineType string + if projectID, err := uuid.Parse(c.Param("id")); err == nil { + if project, err := h.store.GetProject(c.Request.Context(), projectID); err == nil && project != nil { + machineType = project.MachineType + } + } + + // 1. Parse narrative text deterministically (machine-type-aware) + parseResult := iace.ParseNarrative(req.NarrativeText, machineType) // 2. Feed parsed tags into pattern engine // Collect all component IDs for tag resolution diff --git a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go index 11bc400..2bb689f 100644 --- a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go +++ b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go @@ -27,4 +27,9 @@ type HazardPattern struct { ZoneDE string `json:"zone_de,omitempty"` // Gefahrstelle/Zone DefaultSeverity int `json:"default_severity,omitempty"` // 1-5 DefaultExposure int `json:"default_exposure,omitempty"` // 1-5 + // MachineTypes restricts this pattern to specific machine types. + // Empty = fires for all machine types. If set, only fires when the + // project's machine_type is in this list. Prevents e.g. press-specific + // patterns from firing for a cobot project. + MachineTypes []string `json:"machine_types,omitempty"` } diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_press.go b/ai-compliance-sdk/internal/iace/hazard_patterns_press.go index a3c1372..bf41403 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_press.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_press.go @@ -23,7 +23,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Toedliche Quetschverletzung, Amputation von Gliedmassen.", AffectedDE: "Einrichter, Bedienpersonal im Werkzeugeinbauraum.", ZoneDE: "Werkzeugeinbauraum unterhalb des Stoessels.", - DefaultSeverity: 5, DefaultExposure: 2, + DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP046", NameDE: "Quetschen im Werkzeugeinbauraum", NameEN: "Crushing in die space", @@ -38,7 +38,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Toedliche Quetschverletzung, Amputation der oberen Extremitaeten.", AffectedDE: "Einrichter, Werkzeugbauer, Instandhaltungspersonal.", ZoneDE: "Werkzeugeinbauraum zwischen Ober- und Unterwerkzeug.", - DefaultSeverity: 5, DefaultExposure: 3, + DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP047", NameDE: "Oelnebelexposition Atemwege", NameEN: "Oil mist inhalation exposure", @@ -53,7 +53,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Atemwegsreizung, chronische Lungenerkrankung bei Langzeitexposition.", AffectedDE: "Bedienpersonal, Personen im Nahbereich der Presse.", ZoneDE: "Arbeitsbereich rund um die Presse, insbesondere Bedienerseite.", - DefaultSeverity: 3, DefaultExposure: 4, + DefaultSeverity: 3, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP048", NameDE: "Verbrennung durch heisse Werkstuecke", NameEN: "Burns from hot workpieces", @@ -68,7 +68,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Verbrennungen zweiten oder dritten Grades an Haenden und Unterarmen.", AffectedDE: "Bedienpersonal, Einrichter bei Werkzeugwechsel.", ZoneDE: "Entnahmebereich, Werkzeugeinbauraum, Ablagetisch.", - DefaultSeverity: 4, DefaultExposure: 3, + DefaultSeverity: 4, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP049", NameDE: "Schwebende Last (Hubwerk/Aufzug)", NameEN: "Suspended load (hoist/elevator)", @@ -83,7 +83,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Toedliche Verletzung durch herabfallende Last, Knochenbrueche.", AffectedDE: "Personen im Gefahrenbereich unter der schwebenden Last.", ZoneDE: "Bereich unterhalb des Hubwerks, Werkzeugwechselzone.", - DefaultSeverity: 5, DefaultExposure: 2, + DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP050", NameDE: "Einziehen/Scheren Transfersystem", NameEN: "Draw-in/shearing at transfer system", @@ -98,7 +98,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Schnittverletzungen, Amputation von Fingern, Quetschungen.", AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.", ZoneDE: "Transferbereich zwischen den Pressenstationen.", - DefaultSeverity: 4, DefaultExposure: 3, + DefaultSeverity: 4, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP051", NameDE: "Sturzgefahr Auswurfbereich", NameEN: "Fall hazard at ejection area", @@ -114,7 +114,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Knochenbrueche, Prellungen, Kopfverletzungen bei Sturz.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter im Auswurfbereich.", ZoneDE: "Auswurfschacht und angrenzender Bodenbereich.", - DefaultSeverity: 3, DefaultExposure: 4, + DefaultSeverity: 3, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP052", NameDE: "Druckfreisetzung Hydraulikspeicher", NameEN: "Pressure release from hydraulic accumulator", @@ -129,7 +129,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.", AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.", ZoneDE: "Hydraulikaggregat, Speicherbereich, Leitungsfuehrung.", - DefaultSeverity: 5, DefaultExposure: 2, + DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP053", NameDE: "Impulslaerm Pressvorgang", NameEN: "Impact noise during press operation", @@ -144,7 +144,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Laermschwerhoerigkeit, Tinnitus bei Langzeitexposition.", AffectedDE: "Bedienpersonal, Personen in angrenzenden Arbeitsbereichen.", ZoneDE: "Gesamter Pressenbereich, Radius ca. 5-10 m um die Maschine.", - DefaultSeverity: 3, DefaultExposure: 5, + DefaultSeverity: 3, DefaultExposure: 5, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP054", NameDE: "Schwungrad-Restenergie nach Abschaltung", NameEN: "Flywheel residual energy after shutdown", @@ -159,7 +159,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Erfassen durch rotierende Teile, schwere Schnittverletzungen, Skalpierung.", AffectedDE: "Instandhaltungspersonal, Einrichter nach Maschinenstopp.", ZoneDE: "Schwungradbereich, Kupplungsraum, Antriebsseite der Presse.", - DefaultSeverity: 4, DefaultExposure: 2, + DefaultSeverity: 4, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP055", NameDE: "Umgehung Schutzeinrichtung (Pressentuer)", NameEN: "Bypass of safety guard (press door)", @@ -174,7 +174,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Toedliche Quetsch- oder Scherverletzungen bei Eingriff in den Gefahrenbereich.", AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.", ZoneDE: "Gesamter Werkzeugeinbauraum hinter der Schutztuer.", - DefaultSeverity: 5, DefaultExposure: 3, + DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP056", NameDE: "Fehlbedienung Zweihandschaltung", NameEN: "Two-hand control misoperation", @@ -189,7 +189,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Quetschverletzungen der freien Hand im Werkzeugbereich.", AffectedDE: "Bedienpersonal an der Pressenbedienung.", ZoneDE: "Gefahrenbereich zwischen Ober- und Unterwerkzeug.", - DefaultSeverity: 5, DefaultExposure: 3, + DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP057", NameDE: "Hydraulikoelleckage + Rutschgefahr", NameEN: "Hydraulic oil leakage + slip hazard", @@ -204,7 +204,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Sturzverletzungen durch Ausrutschen, Hautreizungen bei Hautkontakt.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter, alle Personen im Pressenbereich.", ZoneDE: "Bodenbereich rund um das Hydraulikaggregat und unter der Presse.", - DefaultSeverity: 2, DefaultExposure: 4, + DefaultSeverity: 2, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, { ID: "HP058", NameDE: "Ergonomische Belastung Kistenwechsel", NameEN: "Ergonomic strain during bin changeover", @@ -219,7 +219,7 @@ func GetPressHazardPatterns() []HazardPattern { HarmDE: "Rueckenverletzungen, Bandscheibenvorfall, Muskel-Skelett-Erkrankungen.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter an der Presse.", ZoneDE: "Auswurfbereich, Palettenstellplatz neben der Presse.", - DefaultSeverity: 2, DefaultExposure: 5, + DefaultSeverity: 2, DefaultExposure: 5, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}, }, } } diff --git a/ai-compliance-sdk/internal/iace/keyword_dictionary.go b/ai-compliance-sdk/internal/iace/keyword_dictionary.go index ba235ac..248b334 100644 --- a/ai-compliance-sdk/internal/iace/keyword_dictionary.go +++ b/ai-compliance-sdk/internal/iace/keyword_dictionary.go @@ -7,6 +7,10 @@ type KeywordEntry struct { ComponentIDs []string // Matched component library IDs (C001-C135) EnergyIDs []string // Matched energy source IDs (EN01-EN20) ExtraTags []string // Additional tags derived from keyword context + // MachineTypes restricts this keyword to specific machine types. + // Empty = matches all machine types. If set, only matches when the + // project's machine_type is in this list. + MachineTypes []string // e.g. ["press", "hydraulic_press"] } // GetKeywordDictionary returns the complete keyword dictionary for @@ -14,13 +18,13 @@ type KeywordEntry struct { // machinery terminology in German and English. func GetKeywordDictionary() []KeywordEntry { return []KeywordEntry{ - // ── Pressen / Umformmaschinen ─────────────────────────────────── - {Keywords: []string{"presse", "press", "umform", "umformung"}, ComponentIDs: []string{"C008", "C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"high_force", "crush_point"}}, - {Keywords: []string{"kniehebel", "toggle"}, ComponentIDs: []string{"C121"}, ExtraTags: []string{"mechanical_transmission"}}, - {Keywords: []string{"stossel", "stoessel", "ram", "slide"}, ComponentIDs: []string{"C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"moving_part", "crush_point", "gravity_risk"}}, - {Keywords: []string{"stempel", "punch", "matrize", "die"}, ComponentIDs: []string{"C126"}, ExtraTags: []string{"crush_point", "cutting_part"}}, - {Keywords: []string{"schwungrad", "flywheel"}, ComponentIDs: []string{"C133"}, EnergyIDs: []string{"EN02", "EN03"}, ExtraTags: []string{"stored_energy", "rotating_part"}}, - {Keywords: []string{"werkzeugeinbauraum", "die space"}, ComponentIDs: []string{"C132"}, ExtraTags: []string{"crush_point", "pinch_point"}}, + // ── Pressen / Umformmaschinen (NUR fuer press/hydraulic_press) ── + {Keywords: []string{"presse", "press", "umform", "umformung"}, ComponentIDs: []string{"C008", "C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"high_force", "crush_point"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press", "stamping_press"}}, + {Keywords: []string{"kniehebel", "toggle"}, ComponentIDs: []string{"C121"}, ExtraTags: []string{"mechanical_transmission"}, MachineTypes: []string{"press"}}, + {Keywords: []string{"stossel", "stoessel", "ram", "slide"}, ComponentIDs: []string{"C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"moving_part", "crush_point", "gravity_risk"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}}, + {Keywords: []string{"stempel", "punch", "matrize", "die"}, ComponentIDs: []string{"C126"}, ExtraTags: []string{"crush_point", "cutting_part"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press", "stamping_press"}}, + {Keywords: []string{"schwungrad", "flywheel"}, ComponentIDs: []string{"C133"}, EnergyIDs: []string{"EN02", "EN03"}, ExtraTags: []string{"stored_energy", "rotating_part"}, MachineTypes: []string{"press", "mechanical_press"}}, + {Keywords: []string{"werkzeugeinbauraum", "die space"}, ComponentIDs: []string{"C132"}, ExtraTags: []string{"crush_point", "pinch_point"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}}, // ── Foerdertechnik ────────────────────────────────────────────── {Keywords: []string{"foerderband", "transportband", "conveyor"}, ComponentIDs: []string{"C003"}, EnergyIDs: []string{"EN01", "EN02"}, ExtraTags: []string{"entanglement_risk"}}, diff --git a/ai-compliance-sdk/internal/iace/narrative_parser.go b/ai-compliance-sdk/internal/iace/narrative_parser.go index 1b46373..18bc7b6 100644 --- a/ai-compliance-sdk/internal/iace/narrative_parser.go +++ b/ai-compliance-sdk/internal/iace/narrative_parser.go @@ -94,7 +94,9 @@ var roleKeywords = map[string]string{ // ParseNarrative extracts components, energy sources, lifecycle phases, // roles, and tags from a machine description text. Fully deterministic, // no LLM required. -func ParseNarrative(text string) ParseResult { +// machineType is optional — if provided, keywords with MachineTypes +// restrictions are only matched when the machine type is in the list. +func ParseNarrative(text string, machineType ...string) ParseResult { result := ParseResult{} if text == "" { return result @@ -122,7 +124,27 @@ func ParseNarrative(text string) ParseResult { seenEnergy := make(map[string]bool) tagSet := make(map[string]bool) + // Resolve machine type for filtering + var mType string + if len(machineType) > 0 { + mType = machineType[0] + } + for _, entry := range dictionary { + // Skip keywords restricted to other machine types + if len(entry.MachineTypes) > 0 && mType != "" { + matched := false + for _, mt := range entry.MachineTypes { + if mt == mType { + matched = true + break + } + } + if !matched { + continue // This keyword is for a different machine type + } + } + for _, kw := range entry.Keywords { kwNorm := strings.ToLower(kw) kwNorm = strings.ReplaceAll(kwNorm, "ä", "ae")