diff --git a/api/core/moderation/keywords/keywords.py b/api/core/moderation/keywords/keywords.py index 339574556d9..1b12e8d27a8 100644 --- a/api/core/moderation/keywords/keywords.py +++ b/api/core/moderation/keywords/keywords.py @@ -42,8 +42,7 @@ class KeywordsModeration(Moderation): if query: inputs["query__"] = query - # Filter out empty values - keywords_list = [keyword for keyword in self.config["keywords"].split("\n") if keyword] + keywords_list = self._keyword_rows(self.config["keywords"]) flagged = self._is_violated(inputs, keywords_list) @@ -59,8 +58,7 @@ class KeywordsModeration(Moderation): raise ValueError("The config is not set.") if self.config["outputs_config"]["enabled"]: - # Filter out empty values - keywords_list = [keyword for keyword in self.config["keywords"].split("\n") if keyword] + keywords_list = self._keyword_rows(self.config["keywords"]) flagged = self._is_violated({"text": text}, keywords_list) preset_response = self.config["outputs_config"]["preset_response"] @@ -69,6 +67,16 @@ class KeywordsModeration(Moderation): flagged=flagged, action=ModerationAction.DIRECT_OUTPUT, preset_response=preset_response ) + @staticmethod + def _keyword_rows(keywords: str) -> list[str]: + """Split the keywords config into stripped, non-empty rows. + + Rows keep surrounding whitespace from pasted configs (for example CRLF + line endings) unless stripped: a keyword ending in "\r" never matches + real text, and a whitespace-only row matches nearly every message. + """ + return [row for row in (row.strip() for row in keywords.split("\n")) if row] + def _is_violated(self, inputs: dict[str, Any], keywords_list: list[str]) -> bool: return any(self._check_keywords_in_value(keywords_list, value) for value in inputs.values()) diff --git a/api/tests/unit_tests/core/moderation/test_content_moderation.py b/api/tests/unit_tests/core/moderation/test_content_moderation.py index 4c668ee96b7..1a2c79d3890 100644 --- a/api/tests/unit_tests/core/moderation/test_content_moderation.py +++ b/api/tests/unit_tests/core/moderation/test_content_moderation.py @@ -127,6 +127,24 @@ class TestKeywordsModeration: with pytest.raises(ValueError, match="inputs_config.preset_response must be less than 100 characters"): KeywordsModeration.validate_config("test-tenant", config) + def test_moderation_for_inputs_crlf_keywords_match(self, keywords_config: dict[str, Any]): + """Keywords pasted with Windows line endings still match.""" + keywords_config["keywords"] = "spam\r\nfree money" + moderation = KeywordsModeration(app_id="app", tenant_id="tenant", config=keywords_config) + + result = moderation.moderation_for_inputs({}, query="this is spam") + + assert result.flagged is True + + def test_moderation_whitespace_only_keyword_row_is_ignored(self, keywords_config: dict[str, Any]): + """A whitespace-only keyword row must not flag every message.""" + keywords_config["keywords"] = "spam\n \n " + moderation = KeywordsModeration(app_id="app", tenant_id="tenant", config=keywords_config) + + result = moderation.moderation_for_inputs({}, query="a perfectly normal message") + + assert result.flagged is False + def test_moderation_for_inputs_no_violation(self, keywords_moderation: KeywordsModeration): """Test input moderation when no keywords are matched.""" inputs = {"user_input": "This is a clean message"} diff --git a/api/tests/unit_tests/core/moderation/test_sensitive_word_filter.py b/api/tests/unit_tests/core/moderation/test_sensitive_word_filter.py index 585a7cf1f71..17a5184c813 100644 --- a/api/tests/unit_tests/core/moderation/test_sensitive_word_filter.py +++ b/api/tests/unit_tests/core/moderation/test_sensitive_word_filter.py @@ -621,13 +621,9 @@ class TestEdgeCases: """Test keyword that is only spaces.""" moderation = self._create_moderation(" ") - # Text without three consecutive spaces should not match - result1 = moderation.moderation_for_inputs({"text": "This has spaces"}) - assert result1.flagged is False + result = moderation.moderation_for_inputs({"text": "This has spaces"}) - # Text with three consecutive spaces should match - result2 = moderation.moderation_for_inputs({"text": "This has spaces"}) - assert result2.flagged is True + assert result.flagged is False def test_config_not_set_error_for_inputs(self): """Test error when config is not set for input moderation."""