This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch 22793-2 in repository https://gitbox.apache.org/repos/asf/camel.git
commit da2f1a03ee3b3b04b30202dc6e5d1c46ede509af Author: Andrea Cosentino <[email protected]> AuthorDate: Fri Dec 19 10:53:39 2025 +0100 CAMEL-22793 - Camel-Langchain4j-Agent: Provide pre-defined guardrails Signed-off-by: Andrea Cosentino <[email protected]> --- .../api/guardrails/CodeInjectionGuardrail.java | 348 +++++++++++++++++++++ .../agent/api/guardrails/Guardrails.java | 136 ++++++++ .../agent/api/guardrails/LanguageGuardrail.java | 294 +++++++++++++++++ .../agent/api/guardrails/NotEmptyGuardrail.java | 138 ++++++++ .../api/guardrails/RegexPatternGuardrail.java | 258 +++++++++++++++ .../agent/api/guardrails/WordCountGuardrail.java | 224 +++++++++++++ .../api/guardrails/CodeInjectionGuardrailTest.java | 178 +++++++++++ .../agent/api/guardrails/GuardrailsTest.java | 82 ++++- .../api/guardrails/LanguageGuardrailTest.java | 156 +++++++++ .../api/guardrails/NotEmptyGuardrailTest.java | 167 ++++++++++ .../api/guardrails/RegexPatternGuardrailTest.java | 180 +++++++++++ .../api/guardrails/WordCountGuardrailTest.java | 184 +++++++++++ .../src/main/docs/langchain4j-agent-component.adoc | 151 +++++++++ 13 files changed, 2495 insertions(+), 1 deletion(-) diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrail.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrail.java new file mode 100644 index 000000000000..1e4148a67c39 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrail.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrail; +import dev.langchain4j.guardrail.InputGuardrailResult; + +/** + * Input guardrail that detects potential code injection attempts in user messages. + * + * <p> + * This guardrail identifies attempts to inject executable code such as: + * </p> + * <ul> + * <li>Shell commands and scripts</li> + * <li>SQL injection patterns</li> + * <li>JavaScript/HTML injection</li> + * <li>Path traversal attempts</li> + * <li>Command chaining patterns</li> + * </ul> + * + * <p> + * Example usage: + * </p> + * + * <pre>{@code + * AgentConfiguration config = new AgentConfiguration() + * .withChatModel(chatModel) + * .withInputGuardrailClasses(List.of(CodeInjectionGuardrail.class)); + * }</pre> + * + * @since 4.17.0 + */ +public class CodeInjectionGuardrail implements InputGuardrail { + + /** + * Types of code injection that can be detected. + */ + public enum InjectionType { + /** Shell command injection (bash, sh, cmd) */ + SHELL_COMMAND, + + /** SQL injection patterns */ + SQL_INJECTION, + + /** JavaScript injection */ + JAVASCRIPT, + + /** HTML/XSS injection */ + HTML_XSS, + + /** Path traversal attacks */ + PATH_TRAVERSAL, + + /** Command chaining (;, &&, ||, |) */ + COMMAND_CHAINING, + + /** Template injection */ + TEMPLATE_INJECTION + } + + private static final List<InjectionPattern> DEFAULT_PATTERNS = Arrays.asList( + // Shell command injection + new InjectionPattern( + InjectionType.SHELL_COMMAND, + Pattern.compile("(?i)\\b(bash|sh|cmd|powershell|exec|eval|system)\\s*\\(")), + new InjectionPattern( + InjectionType.SHELL_COMMAND, + Pattern.compile("(?i)`[^`]+`")), // Backtick execution + new InjectionPattern( + InjectionType.SHELL_COMMAND, + Pattern.compile("(?i)\\$\\([^)]+\\)")), // $() command substitution + new InjectionPattern( + InjectionType.SHELL_COMMAND, + Pattern.compile("(?i)\\b(rm|del|format|mkfs|dd)\\s+(-rf?\\s+)?/")), + + // SQL injection + new InjectionPattern( + InjectionType.SQL_INJECTION, + Pattern.compile("(?i)'\\s*(OR|AND)\\s+['\"]?\\d+['\"]?\\s*=\\s*['\"]?\\d+['\"]?")), + new InjectionPattern( + InjectionType.SQL_INJECTION, + Pattern.compile("(?i)(UNION\\s+(ALL\\s+)?SELECT|INSERT\\s+INTO|DELETE\\s+FROM|DROP\\s+TABLE)")), + new InjectionPattern( + InjectionType.SQL_INJECTION, + Pattern.compile("(?i);\\s*(SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE)\\b")), + new InjectionPattern( + InjectionType.SQL_INJECTION, + Pattern.compile("(?i)--\\s*$")), // SQL comment at end + + // JavaScript injection + new InjectionPattern( + InjectionType.JAVASCRIPT, + Pattern.compile("(?i)<script[^>]*>.*?</script>", Pattern.DOTALL)), + new InjectionPattern( + InjectionType.JAVASCRIPT, + Pattern.compile("(?i)javascript\\s*:")), + new InjectionPattern( + InjectionType.JAVASCRIPT, + Pattern.compile("(?i)\\bon(click|load|error|mouseover|focus)\\s*=")), + + // HTML/XSS + new InjectionPattern( + InjectionType.HTML_XSS, + Pattern.compile("(?i)<(iframe|embed|object|applet|form|input)[^>]*>")), + new InjectionPattern( + InjectionType.HTML_XSS, + Pattern.compile("(?i)\\bstyle\\s*=\\s*['\"].*?(expression|javascript)[^'\"]*['\"]")), + + // Path traversal + new InjectionPattern( + InjectionType.PATH_TRAVERSAL, + Pattern.compile("\\.\\.[\\\\/]")), + new InjectionPattern( + InjectionType.PATH_TRAVERSAL, + Pattern.compile("(?i)%2e%2e[%/\\\\]")), // URL encoded + new InjectionPattern( + InjectionType.PATH_TRAVERSAL, + Pattern.compile("(?i)\\b(etc/passwd|etc/shadow|windows/system32)")), + + // Command chaining + new InjectionPattern( + InjectionType.COMMAND_CHAINING, + Pattern.compile("[;&|]{2}\\s*\\w+")), + new InjectionPattern( + InjectionType.COMMAND_CHAINING, + Pattern.compile(";\\s*(cat|ls|dir|type|rm|del)\\b")), + + // Template injection + new InjectionPattern( + InjectionType.TEMPLATE_INJECTION, + Pattern.compile("\\{\\{.*?\\}\\}")), + new InjectionPattern( + InjectionType.TEMPLATE_INJECTION, + Pattern.compile("\\$\\{.*?\\}")), + new InjectionPattern( + InjectionType.TEMPLATE_INJECTION, + Pattern.compile("<%.*?%>"))); + + private final List<InjectionPattern> patterns; + private final Set<InjectionType> detectTypes; + private final boolean strict; + + /** + * Creates a guardrail that detects all code injection types. + */ + public CodeInjectionGuardrail() { + this(DEFAULT_PATTERNS, EnumSet.allOf(InjectionType.class), false); + } + + /** + * Creates a guardrail with specific configuration. + * + * @param patterns the injection patterns to use + * @param detectTypes the types of injection to detect + * @param strict if true, fail on any match; if false, require context + */ + public CodeInjectionGuardrail(List<InjectionPattern> patterns, Set<InjectionType> detectTypes, boolean strict) { + this.patterns = new ArrayList<>(patterns); + this.detectTypes = EnumSet.copyOf(detectTypes); + this.strict = strict; + } + + /** + * Creates a strict guardrail that fails on any code pattern detection. + * + * @return a new strict CodeInjectionGuardrail + */ + public static CodeInjectionGuardrail strict() { + return new CodeInjectionGuardrail(DEFAULT_PATTERNS, EnumSet.allOf(InjectionType.class), true); + } + + /** + * Creates a guardrail that only detects specific injection types. + * + * @param types the injection types to detect + * @return a new CodeInjectionGuardrail + */ + public static CodeInjectionGuardrail forTypes(InjectionType... types) { + return builder().detectTypes(types).build(); + } + + /** + * Creates a new builder for configuring the guardrail. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public InputGuardrailResult validate(UserMessage userMessage) { + if (userMessage == null || userMessage.singleText() == null) { + return success(); + } + + String text = userMessage.singleText(); + List<InjectionType> detected = new ArrayList<>(); + + for (InjectionPattern pattern : patterns) { + if (!detectTypes.contains(pattern.getType())) { + continue; + } + + if (pattern.getPattern().matcher(text).find()) { + detected.add(pattern.getType()); + + if (strict) { + return failure(String.format( + "Potential code injection detected: %s. " + + "Please remove any code or command patterns from your message.", + pattern.getType())); + } + } + } + + // In non-strict mode, require multiple different types to reduce false positives + if (!strict && detected.size() >= 2) { + return failure(String.format( + "Multiple potential code injection patterns detected: %s. " + + "Please rephrase your message without code-like syntax.", + detected)); + } + + return success(); + } + + /** + * @return true if running in strict mode + */ + public boolean isStrict() { + return strict; + } + + /** + * @return the set of injection types being detected + */ + public Set<InjectionType> getDetectTypes() { + return EnumSet.copyOf(detectTypes); + } + + /** + * Represents a pattern used to detect code injection attempts. + */ + public static class InjectionPattern { + private final InjectionType type; + private final Pattern pattern; + + public InjectionPattern(InjectionType type, Pattern pattern) { + this.type = type; + this.pattern = pattern; + } + + public InjectionType getType() { + return type; + } + + public Pattern getPattern() { + return pattern; + } + } + + /** + * Builder for creating CodeInjectionGuardrail instances. + */ + public static class Builder { + private List<InjectionPattern> patterns = new ArrayList<>(DEFAULT_PATTERNS); + private Set<InjectionType> detectTypes = EnumSet.allOf(InjectionType.class); + private boolean strict = false; + + /** + * Sets the injection types to detect. + * + * @param types the types to detect + * @return this builder + */ + public Builder detectTypes(InjectionType... types) { + this.detectTypes = EnumSet.noneOf(InjectionType.class); + this.detectTypes.addAll(Arrays.asList(types)); + return this; + } + + /** + * Sets strict mode. + * + * @param strict true to fail on any single match + * @return this builder + */ + public Builder strict(boolean strict) { + this.strict = strict; + return this; + } + + /** + * Adds a custom injection pattern. + * + * @param type the injection type + * @param pattern the regex pattern + * @return this builder + */ + public Builder addPattern(InjectionType type, Pattern pattern) { + this.patterns.add(new InjectionPattern(type, pattern)); + return this; + } + + /** + * Clears all default patterns. + * + * @return this builder + */ + public Builder clearPatterns() { + this.patterns.clear(); + return this; + } + + /** + * Builds the guardrail instance. + * + * @return a new CodeInjectionGuardrail instance + */ + public CodeInjectionGuardrail build() { + return new CodeInjectionGuardrail(patterns, detectTypes, strict); + } + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/Guardrails.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/Guardrails.java index 46334ea86b6e..dffb190dfafc 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/Guardrails.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/Guardrails.java @@ -129,9 +129,23 @@ public final class Guardrails { InputLengthGuardrail.class, PiiDetectorGuardrail.class, PromptInjectionGuardrail.class, + CodeInjectionGuardrail.class, KeywordFilterGuardrail.class); } + /** + * Returns a comprehensive set of output guardrails for secure AI responses. + * + * @return list of comprehensive output guardrail classes + */ + public static List<Class<?>> comprehensiveOutputGuardrails() { + return Arrays.asList( + NotEmptyGuardrail.class, + OutputLengthGuardrail.class, + SensitiveDataOutputGuardrail.class, + KeywordOutputFilterGuardrail.class); + } + // ==================== Input Guardrail Factories ==================== /** @@ -199,6 +213,43 @@ public final class Guardrails { return KeywordFilterGuardrail.blocking(words); } + /** + * Creates a code injection guardrail with default settings. + * + * @return a new CodeInjectionGuardrail instance + */ + public static CodeInjectionGuardrail codeInjection() { + return new CodeInjectionGuardrail(); + } + + /** + * Creates a strict code injection guardrail. + * + * @return a new strict CodeInjectionGuardrail instance + */ + public static CodeInjectionGuardrail codeInjectionStrict() { + return CodeInjectionGuardrail.strict(); + } + + /** + * Creates a language guardrail that only allows specific languages. + * + * @param languages the languages to allow + * @return a new LanguageGuardrail instance + */ + public static LanguageGuardrail languageFilter(LanguageGuardrail.Language... languages) { + return LanguageGuardrail.allowOnly(languages); + } + + /** + * Creates a regex pattern guardrail builder. + * + * @return a new RegexPatternGuardrail.Builder instance + */ + public static RegexPatternGuardrail.Builder regexPatternBuilder() { + return RegexPatternGuardrail.builder(); + } + // ==================== Output Guardrail Factories ==================== /** @@ -287,6 +338,55 @@ public final class Guardrails { return KeywordOutputFilterGuardrail.redacting(words); } + /** + * Creates a word count guardrail with minimum word count. + * + * @param minWords minimum required word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail wordCountAtLeast(int minWords) { + return WordCountGuardrail.atLeast(minWords); + } + + /** + * Creates a word count guardrail with maximum word count. + * + * @param maxWords maximum allowed word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail wordCountAtMost(int maxWords) { + return WordCountGuardrail.atMost(maxWords); + } + + /** + * Creates a word count guardrail with min and max word count. + * + * @param minWords minimum required word count + * @param maxWords maximum allowed word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail wordCountBetween(int minWords, int maxWords) { + return WordCountGuardrail.between(minWords, maxWords); + } + + /** + * Creates a not-empty guardrail with default settings. + * + * @return a new NotEmptyGuardrail instance + */ + public static NotEmptyGuardrail notEmpty() { + return new NotEmptyGuardrail(); + } + + /** + * Creates a not-empty guardrail that also detects refusal patterns. + * + * @return a new NotEmptyGuardrail instance with refusal detection + */ + public static NotEmptyGuardrail notEmptyWithRefusalDetection() { + return NotEmptyGuardrail.withRefusalDetection(); + } + // ==================== Fluent Configuration Builder ==================== /** @@ -408,6 +508,42 @@ public final class Guardrails { return withOutputGuardrail(JsonFormatGuardrail.class); } + /** + * Adds code injection detection input guardrail. + * + * @return this builder + */ + public ConfigurationBuilder withCodeInjectionDetection() { + return withInputGuardrail(CodeInjectionGuardrail.class); + } + + /** + * Adds language validation input guardrail. + * + * @return this builder + */ + public ConfigurationBuilder withLanguageValidation() { + return withInputGuardrail(LanguageGuardrail.class); + } + + /** + * Adds not-empty output guardrail. + * + * @return this builder + */ + public ConfigurationBuilder withNotEmptyValidation() { + return withOutputGuardrail(NotEmptyGuardrail.class); + } + + /** + * Adds word count output guardrail. + * + * @return this builder + */ + public ConfigurationBuilder withWordCountValidation() { + return withOutputGuardrail(WordCountGuardrail.class); + } + /** * Builds the AgentConfiguration with the configured guardrails. * diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrail.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrail.java new file mode 100644 index 000000000000..bf8eea8c7b34 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrail.java @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrail; +import dev.langchain4j.guardrail.InputGuardrailResult; + +/** + * Input guardrail that validates the language of user messages. + * + * <p> + * This guardrail uses character set analysis to detect the script/language family of input text. It can be configured + * to allow only specific languages or block specific languages. + * </p> + * + * <p> + * Example usage: + * </p> + * + * <pre>{@code + * // Allow only English + * LanguageGuardrail guardrail = LanguageGuardrail.allowOnly(Language.ENGLISH); + * + * // Allow English and Spanish + * LanguageGuardrail guardrail = LanguageGuardrail.builder() + * .allowedLanguages(Language.ENGLISH, Language.LATIN_SCRIPT) + * .build(); + * }</pre> + * + * @since 4.17.0 + */ +public class LanguageGuardrail implements InputGuardrail { + + /** + * Detected language/script categories. + */ + public enum Language { + /** English and basic Latin characters */ + ENGLISH(Pattern.compile("^[\\p{ASCII}\\s\\p{Punct}]+$")), + + /** Latin script languages (English, Spanish, French, German, etc.) */ + LATIN_SCRIPT(Pattern.compile("[\\p{IsLatin}]")), + + /** Cyrillic script (Russian, Ukrainian, etc.) */ + CYRILLIC(Pattern.compile("[\\p{IsCyrillic}]")), + + /** Chinese characters */ + CHINESE(Pattern.compile("[\\p{IsHan}]")), + + /** Japanese (Hiragana, Katakana, Kanji) */ + JAPANESE(Pattern.compile("[\\p{IsHiragana}\\p{IsKatakana}\\p{IsHan}]")), + + /** Korean (Hangul) */ + KOREAN(Pattern.compile("[\\p{IsHangul}]")), + + /** Arabic script */ + ARABIC(Pattern.compile("[\\p{IsArabic}]")), + + /** Hebrew script */ + HEBREW(Pattern.compile("[\\p{IsHebrew}]")), + + /** Greek script */ + GREEK(Pattern.compile("[\\p{IsGreek}]")), + + /** Thai script */ + THAI(Pattern.compile("[\\p{IsThai}]")), + + /** Devanagari script (Hindi, Sanskrit, etc.) */ + DEVANAGARI(Pattern.compile("[\\p{IsDevanagari}]")); + + private final Pattern pattern; + + Language(Pattern pattern) { + this.pattern = pattern; + } + + public Pattern getPattern() { + return pattern; + } + + /** + * Checks if the text contains characters from this language/script. + */ + public boolean isPresent(String text) { + return pattern.matcher(text).find(); + } + } + + private final Set<Language> allowedLanguages; + private final Set<Language> blockedLanguages; + private final boolean allowMixed; + private final double minLanguageRatio; + + /** + * Creates a guardrail that allows all languages. + */ + public LanguageGuardrail() { + this(new HashSet<>(), new HashSet<>(), true, 0.0); + } + + /** + * Creates a guardrail with specific configuration. + * + * @param allowedLanguages languages to allow (empty = allow all) + * @param blockedLanguages languages to block + * @param allowMixed whether to allow mixed language content + * @param minLanguageRatio minimum ratio of allowed language characters (0.0-1.0) + */ + public LanguageGuardrail(Set<Language> allowedLanguages, Set<Language> blockedLanguages, + boolean allowMixed, double minLanguageRatio) { + this.allowedLanguages = new HashSet<>(allowedLanguages); + this.blockedLanguages = new HashSet<>(blockedLanguages); + this.allowMixed = allowMixed; + this.minLanguageRatio = minLanguageRatio; + } + + /** + * Creates a guardrail that only allows specific languages. + * + * @param languages the languages to allow + * @return a new LanguageGuardrail instance + */ + public static LanguageGuardrail allowOnly(Language... languages) { + return builder().allowedLanguages(languages).build(); + } + + /** + * Creates a guardrail that blocks specific languages. + * + * @param languages the languages to block + * @return a new LanguageGuardrail instance + */ + public static LanguageGuardrail block(Language... languages) { + return builder().blockedLanguages(languages).build(); + } + + /** + * Creates a new builder for configuring the guardrail. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public InputGuardrailResult validate(UserMessage userMessage) { + if (userMessage == null || userMessage.singleText() == null) { + return success(); + } + + String text = userMessage.singleText(); + + // Check for blocked languages + for (Language blocked : blockedLanguages) { + if (blocked.isPresent(text)) { + return failure(String.format( + "Message contains blocked language/script: %s", blocked.name())); + } + } + + // If no allowed languages specified, allow all (that aren't blocked) + if (allowedLanguages.isEmpty()) { + return success(); + } + + // Check if any allowed language is present + boolean hasAllowedLanguage = false; + Set<Language> detectedLanguages = new HashSet<>(); + + for (Language lang : Language.values()) { + if (lang.isPresent(text)) { + detectedLanguages.add(lang); + if (allowedLanguages.contains(lang)) { + hasAllowedLanguage = true; + } + } + } + + if (!hasAllowedLanguage) { + return failure(String.format( + "Message language not allowed. Allowed languages: %s", allowedLanguages)); + } + + // Check for mixed content if not allowed + if (!allowMixed && detectedLanguages.size() > 1) { + // Check if all detected languages are in allowed set + for (Language detected : detectedLanguages) { + if (!allowedLanguages.contains(detected) && detected != Language.ENGLISH) { + return failure("Mixed language content is not allowed."); + } + } + } + + return success(); + } + + /** + * @return the set of allowed languages + */ + public Set<Language> getAllowedLanguages() { + return new HashSet<>(allowedLanguages); + } + + /** + * @return the set of blocked languages + */ + public Set<Language> getBlockedLanguages() { + return new HashSet<>(blockedLanguages); + } + + /** + * Builder for creating LanguageGuardrail instances. + */ + public static class Builder { + private Set<Language> allowedLanguages = new HashSet<>(); + private Set<Language> blockedLanguages = new HashSet<>(); + private boolean allowMixed = true; + private double minLanguageRatio = 0.0; + + /** + * Sets the allowed languages. + * + * @param languages the languages to allow + * @return this builder + */ + public Builder allowedLanguages(Language... languages) { + this.allowedLanguages.addAll(Arrays.asList(languages)); + return this; + } + + /** + * Sets the blocked languages. + * + * @param languages the languages to block + * @return this builder + */ + public Builder blockedLanguages(Language... languages) { + this.blockedLanguages.addAll(Arrays.asList(languages)); + return this; + } + + /** + * Sets whether mixed language content is allowed. + * + * @param allowMixed true to allow mixed content + * @return this builder + */ + public Builder allowMixed(boolean allowMixed) { + this.allowMixed = allowMixed; + return this; + } + + /** + * Sets the minimum ratio of allowed language characters. + * + * @param ratio minimum ratio (0.0-1.0) + * @return this builder + */ + public Builder minLanguageRatio(double ratio) { + this.minLanguageRatio = ratio; + return this; + } + + /** + * Builds the guardrail instance. + * + * @return a new LanguageGuardrail instance + */ + public LanguageGuardrail build() { + return new LanguageGuardrail(allowedLanguages, blockedLanguages, allowMixed, minLanguageRatio); + } + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrail.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrail.java new file mode 100644 index 000000000000..97cc38fd0dbc --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrail.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.OutputGuardrail; +import dev.langchain4j.guardrail.OutputGuardrailResult; + +/** + * Output guardrail that ensures AI responses are not empty or contain only whitespace. + * + * <p> + * This simple guardrail validates that the LLM actually produced a meaningful response. It can also check for common + * "refusal" patterns where the LLM declines to answer. + * </p> + * + * <p> + * Example usage: + * </p> + * + * <pre>{@code + * AgentConfiguration config = new AgentConfiguration() + * .withChatModel(chatModel) + * .withOutputGuardrailClasses(List.of(NotEmptyGuardrail.class)); + * }</pre> + * + * @since 4.17.0 + */ +public class NotEmptyGuardrail implements OutputGuardrail { + + private final boolean detectRefusals; + private final int minMeaningfulLength; + + /** + * Default refusal phrases to detect. + */ + private static final String[] REFUSAL_PATTERNS = { + "I cannot", "I can't", "I'm unable to", "I am unable to", + "I don't have", "I do not have", "I'm not able to", + "I apologize, but I cannot", "Sorry, but I cannot", + "I'm sorry, I cannot", "I'm afraid I cannot" + }; + + /** + * Creates a guardrail with default settings. + */ + public NotEmptyGuardrail() { + this(false, 1); + } + + /** + * Creates a guardrail with custom settings. + * + * @param detectRefusals whether to detect refusal patterns + * @param minMeaningfulLength minimum length for a meaningful response + */ + public NotEmptyGuardrail(boolean detectRefusals, int minMeaningfulLength) { + this.detectRefusals = detectRefusals; + this.minMeaningfulLength = Math.max(1, minMeaningfulLength); + } + + /** + * Creates a guardrail that also detects refusal patterns. + * + * @return a new NotEmptyGuardrail that detects refusals + */ + public static NotEmptyGuardrail withRefusalDetection() { + return new NotEmptyGuardrail(true, 1); + } + + /** + * Creates a guardrail with a minimum meaningful length. + * + * @param minLength minimum character length for meaningful response + * @return a new NotEmptyGuardrail instance + */ + public static NotEmptyGuardrail withMinLength(int minLength) { + return new NotEmptyGuardrail(false, minLength); + } + + @Override + public OutputGuardrailResult validate(AiMessage aiMessage) { + if (aiMessage == null || aiMessage.text() == null) { + return retry("AI response cannot be null. Please try again."); + } + + String text = aiMessage.text().trim(); + + if (text.isEmpty()) { + return retry("AI response is empty. Please provide a meaningful response."); + } + + if (text.length() < minMeaningfulLength) { + return retry(String.format( + "AI response is too short (%d chars). Please provide a more complete response.", + text.length())); + } + + if (detectRefusals) { + String lowerText = text.toLowerCase(); + for (String pattern : REFUSAL_PATTERNS) { + if (lowerText.startsWith(pattern.toLowerCase())) { + return retry("AI declined to answer. Please rephrase the question or try again."); + } + } + } + + return success(); + } + + /** + * @return whether refusal detection is enabled + */ + public boolean isDetectRefusals() { + return detectRefusals; + } + + /** + * @return the minimum meaningful length + */ + public int getMinMeaningfulLength() { + return minMeaningfulLength; + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrail.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrail.java new file mode 100644 index 000000000000..6e22a42d2b45 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrail.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrail; +import dev.langchain4j.guardrail.InputGuardrailResult; + +/** + * A flexible input guardrail that uses custom regex patterns for validation. + * + * <p> + * This guardrail allows you to define custom patterns to either block (deny patterns) or require (allow patterns) in + * user messages. It's useful when you need custom validation beyond the built-in guardrails. + * </p> + * + * <p> + * Example usage: + * </p> + * + * <pre>{@code + * // Block messages containing URLs + * RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + * .denyPattern("https?://[^\\s]+", "URLs are not allowed") + * .build(); + * + * // Require messages to contain a ticket number + * RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + * .requirePattern("TICKET-\\d+", "Please include a ticket number (e.g., TICKET-123)") + * .build(); + * }</pre> + * + * @since 4.17.0 + */ +public class RegexPatternGuardrail implements InputGuardrail { + + private final List<PatternRule> denyPatterns; + private final List<PatternRule> requirePatterns; + private final boolean failOnFirstMatch; + + /** + * Creates an empty guardrail with no patterns. + */ + public RegexPatternGuardrail() { + this(new ArrayList<>(), new ArrayList<>(), true); + } + + /** + * Creates a guardrail with the specified patterns. + * + * @param denyPatterns patterns that will cause validation to fail if matched + * @param requirePatterns patterns that must be present for validation to pass + * @param failOnFirstMatch if true, stop checking after first failure + */ + public RegexPatternGuardrail(List<PatternRule> denyPatterns, List<PatternRule> requirePatterns, + boolean failOnFirstMatch) { + this.denyPatterns = new ArrayList<>(denyPatterns); + this.requirePatterns = new ArrayList<>(requirePatterns); + this.failOnFirstMatch = failOnFirstMatch; + } + + /** + * Creates a guardrail that blocks messages matching the pattern. + * + * @param pattern the regex pattern to block + * @param errorMessage the error message to show when blocked + * @return a new RegexPatternGuardrail instance + */ + public static RegexPatternGuardrail blocking(String pattern, String errorMessage) { + return builder().denyPattern(pattern, errorMessage).build(); + } + + /** + * Creates a guardrail that requires messages to match the pattern. + * + * @param pattern the regex pattern to require + * @param errorMessage the error message when pattern is missing + * @return a new RegexPatternGuardrail instance + */ + public static RegexPatternGuardrail requiring(String pattern, String errorMessage) { + return builder().requirePattern(pattern, errorMessage).build(); + } + + /** + * Creates a new builder for configuring the guardrail. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public InputGuardrailResult validate(UserMessage userMessage) { + if (userMessage == null || userMessage.singleText() == null) { + return success(); + } + + String text = userMessage.singleText(); + List<String> errors = new ArrayList<>(); + + // Check deny patterns (should NOT match) + for (PatternRule rule : denyPatterns) { + if (rule.getPattern().matcher(text).find()) { + if (failOnFirstMatch) { + return failure(rule.getErrorMessage()); + } + errors.add(rule.getErrorMessage()); + } + } + + // Check require patterns (MUST match) + for (PatternRule rule : requirePatterns) { + if (!rule.getPattern().matcher(text).find()) { + if (failOnFirstMatch) { + return failure(rule.getErrorMessage()); + } + errors.add(rule.getErrorMessage()); + } + } + + if (!errors.isEmpty()) { + return failure(String.join("; ", errors)); + } + + return success(); + } + + /** + * @return the list of deny patterns + */ + public List<PatternRule> getDenyPatterns() { + return new ArrayList<>(denyPatterns); + } + + /** + * @return the list of require patterns + */ + public List<PatternRule> getRequirePatterns() { + return new ArrayList<>(requirePatterns); + } + + /** + * Represents a pattern rule with an error message. + */ + public static class PatternRule { + private final Pattern pattern; + private final String errorMessage; + + public PatternRule(Pattern pattern, String errorMessage) { + this.pattern = pattern; + this.errorMessage = errorMessage; + } + + public Pattern getPattern() { + return pattern; + } + + public String getErrorMessage() { + return errorMessage; + } + } + + /** + * Builder for creating RegexPatternGuardrail instances. + */ + public static class Builder { + private List<PatternRule> denyPatterns = new ArrayList<>(); + private List<PatternRule> requirePatterns = new ArrayList<>(); + private boolean failOnFirstMatch = true; + + /** + * Adds a pattern that will cause validation to fail if matched. + * + * @param regex the regex pattern string + * @param errorMessage the error message when matched + * @return this builder + */ + public Builder denyPattern(String regex, String errorMessage) { + denyPatterns.add(new PatternRule(Pattern.compile(regex), errorMessage)); + return this; + } + + /** + * Adds a pattern that will cause validation to fail if matched. + * + * @param pattern the compiled pattern + * @param errorMessage the error message when matched + * @return this builder + */ + public Builder denyPattern(Pattern pattern, String errorMessage) { + denyPatterns.add(new PatternRule(pattern, errorMessage)); + return this; + } + + /** + * Adds a pattern that must be present for validation to pass. + * + * @param regex the regex pattern string + * @param errorMessage the error message when not matched + * @return this builder + */ + public Builder requirePattern(String regex, String errorMessage) { + requirePatterns.add(new PatternRule(Pattern.compile(regex), errorMessage)); + return this; + } + + /** + * Adds a pattern that must be present for validation to pass. + * + * @param pattern the compiled pattern + * @param errorMessage the error message when not matched + * @return this builder + */ + public Builder requirePattern(Pattern pattern, String errorMessage) { + requirePatterns.add(new PatternRule(pattern, errorMessage)); + return this; + } + + /** + * Sets whether to fail on first pattern match or collect all errors. + * + * @param failOnFirstMatch true to stop at first failure + * @return this builder + */ + public Builder failOnFirstMatch(boolean failOnFirstMatch) { + this.failOnFirstMatch = failOnFirstMatch; + return this; + } + + /** + * Builds the guardrail instance. + * + * @return a new RegexPatternGuardrail instance + */ + public RegexPatternGuardrail build() { + return new RegexPatternGuardrail(denyPatterns, requirePatterns, failOnFirstMatch); + } + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrail.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrail.java new file mode 100644 index 000000000000..b2bd8ced8bbe --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrail.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.OutputGuardrail; +import dev.langchain4j.guardrail.OutputGuardrailResult; + +/** + * Output guardrail that validates the word count of AI responses. + * + * <p> + * This guardrail ensures responses meet word count requirements, useful for: + * </p> + * <ul> + * <li>Ensuring detailed responses (minimum words)</li> + * <li>Keeping responses concise (maximum words)</li> + * <li>Enforcing specific response formats</li> + * </ul> + * + * <p> + * Example usage: + * </p> + * + * <pre>{@code + * // Ensure responses are at least 50 words + * WordCountGuardrail guardrail = WordCountGuardrail.atLeast(50); + * + * // Ensure responses are between 100 and 500 words + * WordCountGuardrail guardrail = WordCountGuardrail.between(100, 500); + * }</pre> + * + * @since 4.17.0 + */ +public class WordCountGuardrail implements OutputGuardrail { + + /** + * Default maximum word count. + */ + public static final int DEFAULT_MAX_WORDS = 10000; + + /** + * Default minimum word count. + */ + public static final int DEFAULT_MIN_WORDS = 1; + + private final int minWords; + private final int maxWords; + + /** + * Creates a guardrail with default word limits. + */ + public WordCountGuardrail() { + this(DEFAULT_MIN_WORDS, DEFAULT_MAX_WORDS); + } + + /** + * Creates a guardrail with custom word limits. + * + * @param minWords minimum required word count + * @param maxWords maximum allowed word count + */ + public WordCountGuardrail(int minWords, int maxWords) { + if (minWords < 0) { + throw new IllegalArgumentException("minWords cannot be negative"); + } + if (maxWords <= 0) { + throw new IllegalArgumentException("maxWords must be positive"); + } + if (minWords > maxWords) { + throw new IllegalArgumentException("minWords cannot exceed maxWords"); + } + this.minWords = minWords; + this.maxWords = maxWords; + } + + /** + * Creates a guardrail requiring at least the specified number of words. + * + * @param minWords minimum required word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail atLeast(int minWords) { + return new WordCountGuardrail(minWords, DEFAULT_MAX_WORDS); + } + + /** + * Creates a guardrail allowing at most the specified number of words. + * + * @param maxWords maximum allowed word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail atMost(int maxWords) { + return new WordCountGuardrail(DEFAULT_MIN_WORDS, maxWords); + } + + /** + * Creates a guardrail with both minimum and maximum word limits. + * + * @param minWords minimum required word count + * @param maxWords maximum allowed word count + * @return a new WordCountGuardrail instance + */ + public static WordCountGuardrail between(int minWords, int maxWords) { + return new WordCountGuardrail(minWords, maxWords); + } + + /** + * Creates a new builder for configuring the guardrail. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public OutputGuardrailResult validate(AiMessage aiMessage) { + if (aiMessage == null || aiMessage.text() == null) { + return fatal("AI response cannot be null or empty"); + } + + String text = aiMessage.text().trim(); + int wordCount = countWords(text); + + if (wordCount < minWords) { + return retry(String.format( + "Response too brief: %d words (minimum: %d). Please provide a more detailed response.", + wordCount, minWords)); + } + + if (wordCount > maxWords) { + return retry(String.format( + "Response too verbose: %d words (maximum: %d). Please provide a more concise response.", + wordCount, maxWords)); + } + + return success(); + } + + /** + * Counts the number of words in the text. + */ + private int countWords(String text) { + if (text == null || text.isEmpty()) { + return 0; + } + String[] words = text.split("\\s+"); + int count = 0; + for (String word : words) { + if (!word.isEmpty()) { + count++; + } + } + return count; + } + + /** + * @return the minimum required word count + */ + public int getMinWords() { + return minWords; + } + + /** + * @return the maximum allowed word count + */ + public int getMaxWords() { + return maxWords; + } + + /** + * Builder for creating WordCountGuardrail instances. + */ + public static class Builder { + private int minWords = 0; + private int maxWords = Integer.MAX_VALUE; + + /** + * Sets the minimum word count. + * + * @param minWords minimum required word count + * @return this builder + */ + public Builder minWords(int minWords) { + this.minWords = minWords; + return this; + } + + /** + * Sets the maximum word count. + * + * @param maxWords maximum allowed word count + * @return this builder + */ + public Builder maxWords(int maxWords) { + this.maxWords = maxWords; + return this; + } + + /** + * Builds the guardrail instance. + * + * @return a new WordCountGuardrail instance + */ + public WordCountGuardrail build() { + return new WordCountGuardrail(minWords, maxWords); + } + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrailTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrailTest.java new file mode 100644 index 000000000000..536b22d65a2f --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/CodeInjectionGuardrailTest.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrailResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CodeInjectionGuardrailTest { + + @Test + void testCleanMessage() { + CodeInjectionGuardrail guardrail = new CodeInjectionGuardrail(); + + assertTrue(guardrail.validate(UserMessage.from("Hello, how are you today?")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("What is the weather like?")).isSuccess()); + } + + @Test + void testShellCommandInjection() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Run bash('ls -la')")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Execute system('rm -rf /')")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Try exec('cat /etc/passwd')")).isSuccess()); + } + + @Test + void testBacktickExecution() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Run `ls -la`")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Execute `cat /etc/passwd`")).isSuccess()); + } + + @Test + void testCommandSubstitution() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Run $(whoami)")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Execute $(cat /etc/passwd)")).isSuccess()); + } + + @Test + void testSqlInjection() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("' OR '1'='1")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("UNION SELECT * FROM users")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("'; DROP TABLE users--")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("INSERT INTO users VALUES")).isSuccess()); + } + + @Test + void testJavaScriptInjection() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("<script>alert('XSS')</script>")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("javascript:alert('XSS')")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("onclick=alert('XSS')")).isSuccess()); + } + + @Test + void testHtmlXssInjection() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("<iframe src='evil.com'></iframe>")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("<embed src='evil.swf'>")).isSuccess()); + } + + @Test + void testPathTraversal() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Read file ../../../etc/passwd")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Access ..\\..\\windows\\system32")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Get %2e%2e/etc/passwd")).isSuccess()); + } + + @Test + void testCommandChaining() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Run && cat /etc/passwd")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("; cat /etc/passwd")).isSuccess()); + } + + @Test + void testTemplateInjection() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.strict(); + + assertFalse(guardrail.validate(UserMessage.from("Use {{constructor.constructor}}")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Try ${7*7}")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Execute <% code %>")).isSuccess()); + } + + @Test + void testNonStrictModeRequiresMultipleMatches() { + CodeInjectionGuardrail guardrail = new CodeInjectionGuardrail(); + + // Single pattern match should pass in non-strict mode + assertTrue(guardrail.validate(UserMessage.from("Use {{template}}")).isSuccess()); + + // Multiple different types should fail + assertFalse(guardrail.validate(UserMessage.from("Run {{template}} and ../../../etc/passwd")).isSuccess()); + } + + @Test + void testForSpecificTypes() { + // Use builder with strict mode to fail on single match + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.builder() + .detectTypes(CodeInjectionGuardrail.InjectionType.SQL_INJECTION) + .strict(true) + .build(); + + // SQL injection should fail + assertFalse(guardrail.validate(UserMessage.from("' OR '1'='1")).isSuccess()); + + // Other types should pass since we only detect SQL + assertTrue(guardrail.validate(UserMessage.from("<script>alert('XSS')</script>")).isSuccess()); + } + + @Test + void testNullMessage() { + CodeInjectionGuardrail guardrail = new CodeInjectionGuardrail(); + + InputGuardrailResult result = guardrail.validate((UserMessage) null); + assertTrue(result.isSuccess()); + } + + @Test + void testIsStrict() { + CodeInjectionGuardrail defaultGuard = new CodeInjectionGuardrail(); + CodeInjectionGuardrail strictGuard = CodeInjectionGuardrail.strict(); + + assertFalse(defaultGuard.isStrict()); + assertTrue(strictGuard.isStrict()); + } + + @Test + void testGetDetectTypes() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.forTypes( + CodeInjectionGuardrail.InjectionType.SQL_INJECTION, + CodeInjectionGuardrail.InjectionType.SHELL_COMMAND); + + assertTrue(guardrail.getDetectTypes().contains(CodeInjectionGuardrail.InjectionType.SQL_INJECTION)); + assertTrue(guardrail.getDetectTypes().contains(CodeInjectionGuardrail.InjectionType.SHELL_COMMAND)); + assertFalse(guardrail.getDetectTypes().contains(CodeInjectionGuardrail.InjectionType.JAVASCRIPT)); + } + + @Test + void testBuilderWithCustomPattern() { + CodeInjectionGuardrail guardrail = CodeInjectionGuardrail.builder() + .detectTypes(CodeInjectionGuardrail.InjectionType.SHELL_COMMAND) + .strict(true) + .build(); + + assertTrue(guardrail.isStrict()); + assertTrue(guardrail.getDetectTypes().contains(CodeInjectionGuardrail.InjectionType.SHELL_COMMAND)); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/GuardrailsTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/GuardrailsTest.java index c2865e730bd9..8f4c33d670f8 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/GuardrailsTest.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/GuardrailsTest.java @@ -63,13 +63,26 @@ class GuardrailsTest { List<Class<?>> guardrails = Guardrails.strictInputGuardrails(); assertNotNull(guardrails); - assertEquals(4, guardrails.size()); + assertEquals(5, guardrails.size()); assertTrue(guardrails.contains(InputLengthGuardrail.class)); assertTrue(guardrails.contains(PiiDetectorGuardrail.class)); assertTrue(guardrails.contains(PromptInjectionGuardrail.class)); + assertTrue(guardrails.contains(CodeInjectionGuardrail.class)); assertTrue(guardrails.contains(KeywordFilterGuardrail.class)); } + @Test + void testComprehensiveOutputGuardrails() { + List<Class<?>> guardrails = Guardrails.comprehensiveOutputGuardrails(); + + assertNotNull(guardrails); + assertEquals(4, guardrails.size()); + assertTrue(guardrails.contains(NotEmptyGuardrail.class)); + assertTrue(guardrails.contains(OutputLengthGuardrail.class)); + assertTrue(guardrails.contains(SensitiveDataOutputGuardrail.class)); + assertTrue(guardrails.contains(KeywordOutputFilterGuardrail.class)); + } + @Test void testInputGuardrailFactories() { assertNotNull(Guardrails.inputLength()); @@ -165,4 +178,71 @@ class GuardrailsTest { assertEquals(3, config.getInputGuardrailClasses().size()); assertEquals(1, config.getOutputGuardrailClasses().size()); } + + @Test + void testCodeInjectionFactories() { + assertNotNull(Guardrails.codeInjection()); + assertFalse(Guardrails.codeInjection().isStrict()); + assertTrue(Guardrails.codeInjectionStrict().isStrict()); + } + + @Test + void testLanguageFilterFactory() { + LanguageGuardrail guardrail = Guardrails.languageFilter(LanguageGuardrail.Language.ENGLISH); + assertNotNull(guardrail); + assertTrue(guardrail.getAllowedLanguages().contains(LanguageGuardrail.Language.ENGLISH)); + } + + @Test + void testRegexPatternBuilderFactory() { + RegexPatternGuardrail.Builder builder = Guardrails.regexPatternBuilder(); + assertNotNull(builder); + + RegexPatternGuardrail guardrail = builder + .denyPattern("test", "Error") + .build(); + assertNotNull(guardrail); + } + + @Test + void testWordCountFactories() { + WordCountGuardrail atLeast = Guardrails.wordCountAtLeast(10); + assertEquals(10, atLeast.getMinWords()); + + WordCountGuardrail atMost = Guardrails.wordCountAtMost(100); + assertEquals(100, atMost.getMaxWords()); + + WordCountGuardrail between = Guardrails.wordCountBetween(5, 50); + assertEquals(5, between.getMinWords()); + assertEquals(50, between.getMaxWords()); + } + + @Test + void testNotEmptyFactories() { + NotEmptyGuardrail notEmpty = Guardrails.notEmpty(); + assertNotNull(notEmpty); + assertFalse(notEmpty.isDetectRefusals()); + + NotEmptyGuardrail withRefusal = Guardrails.notEmptyWithRefusalDetection(); + assertNotNull(withRefusal); + assertTrue(withRefusal.isDetectRefusals()); + } + + @Test + void testConfigurationBuilderWithNewGuardrails() { + AgentConfiguration config = Guardrails.configure() + .withCodeInjectionDetection() + .withLanguageValidation() + .withNotEmptyValidation() + .withWordCountValidation() + .build(); + + assertNotNull(config); + assertEquals(2, config.getInputGuardrailClasses().size()); + assertEquals(2, config.getOutputGuardrailClasses().size()); + assertTrue(config.getInputGuardrailClasses().contains(CodeInjectionGuardrail.class)); + assertTrue(config.getInputGuardrailClasses().contains(LanguageGuardrail.class)); + assertTrue(config.getOutputGuardrailClasses().contains(NotEmptyGuardrail.class)); + assertTrue(config.getOutputGuardrailClasses().contains(WordCountGuardrail.class)); + } } diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrailTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrailTest.java new file mode 100644 index 000000000000..3c2f65737e9e --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/LanguageGuardrailTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrailResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LanguageGuardrailTest { + + @Test + void testAllowAllLanguagesByDefault() { + LanguageGuardrail guardrail = new LanguageGuardrail(); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("Привет мир")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("你好世界")).isSuccess()); + } + + @Test + void testAllowOnlyEnglish() { + LanguageGuardrail guardrail = LanguageGuardrail.allowOnly(LanguageGuardrail.Language.ENGLISH); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Привет мир")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("你好世界")).isSuccess()); + } + + @Test + void testAllowLatinScript() { + LanguageGuardrail guardrail = LanguageGuardrail.allowOnly(LanguageGuardrail.Language.LATIN_SCRIPT); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("Hola mundo")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("Bonjour le monde")).isSuccess()); + } + + @Test + void testBlockCyrillic() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.CYRILLIC); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Привет мир")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Hello Привет")).isSuccess()); + } + + @Test + void testBlockChinese() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.CHINESE); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("你好世界")).isSuccess()); + } + + @Test + void testBlockArabic() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.ARABIC); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("مرحبا بالعالم")).isSuccess()); + } + + @Test + void testMultipleAllowedLanguages() { + LanguageGuardrail guardrail = LanguageGuardrail.builder() + .allowedLanguages(LanguageGuardrail.Language.ENGLISH, LanguageGuardrail.Language.LATIN_SCRIPT) + .build(); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("Bonjour")).isSuccess()); + } + + @Test + void testMixedContentAllowed() { + LanguageGuardrail guardrail = LanguageGuardrail.builder() + .allowedLanguages(LanguageGuardrail.Language.ENGLISH, LanguageGuardrail.Language.LATIN_SCRIPT) + .allowMixed(true) + .build(); + + assertTrue(guardrail.validate(UserMessage.from("Hello Bonjour")).isSuccess()); + } + + @Test + void testNullMessage() { + LanguageGuardrail guardrail = new LanguageGuardrail(); + + InputGuardrailResult result = guardrail.validate((UserMessage) null); + assertTrue(result.isSuccess()); + } + + @Test + void testJapaneseDetection() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.JAPANESE); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("こんにちは")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("カタカナ")).isSuccess()); + } + + @Test + void testKoreanDetection() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.KOREAN); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("안녕하세요")).isSuccess()); + } + + @Test + void testHebrewDetection() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.HEBREW); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("שלום עולם")).isSuccess()); + } + + @Test + void testGreekDetection() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.GREEK); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Γειά σου κόσμε")).isSuccess()); + } + + @Test + void testGetAllowedLanguages() { + LanguageGuardrail guardrail = LanguageGuardrail.allowOnly( + LanguageGuardrail.Language.ENGLISH, LanguageGuardrail.Language.LATIN_SCRIPT); + + assertTrue(guardrail.getAllowedLanguages().contains(LanguageGuardrail.Language.ENGLISH)); + assertTrue(guardrail.getAllowedLanguages().contains(LanguageGuardrail.Language.LATIN_SCRIPT)); + } + + @Test + void testGetBlockedLanguages() { + LanguageGuardrail guardrail = LanguageGuardrail.block(LanguageGuardrail.Language.CYRILLIC); + + assertTrue(guardrail.getBlockedLanguages().contains(LanguageGuardrail.Language.CYRILLIC)); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrailTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrailTest.java new file mode 100644 index 000000000000..3354fe72120d --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/NotEmptyGuardrailTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.OutputGuardrailResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NotEmptyGuardrailTest { + + @Test + void testValidResponse() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(); + + assertTrue(guardrail.validate(AiMessage.from("Hello, how can I help you?")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("Here is your answer.")).isSuccess()); + } + + @Test + void testEmptyResponse() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(); + + assertFalse(guardrail.validate(AiMessage.from("")).isSuccess()); + } + + @Test + void testWhitespaceOnlyResponse() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(); + + assertFalse(guardrail.validate(AiMessage.from(" ")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("\t\n")).isSuccess()); + } + + @Test + void testNullMessage() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(); + + OutputGuardrailResult result = guardrail.validate((AiMessage) null); + assertFalse(result.isSuccess()); + } + + @Test + void testRefusalDetectionEnabled() { + NotEmptyGuardrail guardrail = NotEmptyGuardrail.withRefusalDetection(); + + // Normal responses should pass + assertTrue(guardrail.validate(AiMessage.from("Here is your answer.")).isSuccess()); + + // Refusal patterns should fail + assertFalse(guardrail.validate(AiMessage.from("I cannot help with that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I can't provide that information.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I'm unable to assist with that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I am unable to do that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I don't have access to that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I'm not able to help.")).isSuccess()); + } + + @Test + void testRefusalDetectionDisabledByDefault() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(); + + // Refusal patterns should pass when detection is disabled + assertTrue(guardrail.validate(AiMessage.from("I cannot help with that.")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("I can't provide that information.")).isSuccess()); + } + + @Test + void testMinMeaningfulLength() { + NotEmptyGuardrail guardrail = NotEmptyGuardrail.withMinLength(10); + + // Too short + assertFalse(guardrail.validate(AiMessage.from("Hi")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("Yes")).isSuccess()); + + // Just at minimum + assertTrue(guardrail.validate(AiMessage.from("0123456789")).isSuccess()); + + // Above minimum + assertTrue(guardrail.validate(AiMessage.from("This is a proper response.")).isSuccess()); + } + + @Test + void testCustomConfiguration() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(true, 20); + + // Too short + assertFalse(guardrail.validate(AiMessage.from("Short")).isSuccess()); + + // Refusal + assertFalse(guardrail.validate(AiMessage.from("I cannot help with that request at all.")).isSuccess()); + + // Valid response + assertTrue(guardrail.validate(AiMessage.from("This is a proper response that is long enough.")).isSuccess()); + } + + @Test + void testMinLengthEnforcesAtLeastOne() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(false, 0); + + // Min length should be enforced to at least 1 + assertEquals(1, guardrail.getMinMeaningfulLength()); + } + + @Test + void testNegativeMinLengthTreatedAsOne() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(false, -5); + + assertEquals(1, guardrail.getMinMeaningfulLength()); + } + + @Test + void testGetters() { + NotEmptyGuardrail guardrail = new NotEmptyGuardrail(true, 50); + + assertTrue(guardrail.isDetectRefusals()); + assertEquals(50, guardrail.getMinMeaningfulLength()); + } + + @Test + void testRefusalPatternsCaseInsensitive() { + NotEmptyGuardrail guardrail = NotEmptyGuardrail.withRefusalDetection(); + + // Test case insensitivity + assertFalse(guardrail.validate(AiMessage.from("I CANNOT help with that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("i cannot help with that.")).isSuccess()); + } + + @Test + void testRefusalMustBeAtStart() { + NotEmptyGuardrail guardrail = NotEmptyGuardrail.withRefusalDetection(); + + // Refusal at start should fail + assertFalse(guardrail.validate(AiMessage.from("I cannot do that for you.")).isSuccess()); + + // Refusal not at start should pass + assertTrue( + guardrail.validate(AiMessage.from("While I cannot do everything, here is what I can help with.")).isSuccess()); + } + + @Test + void testApologyRefusals() { + NotEmptyGuardrail guardrail = NotEmptyGuardrail.withRefusalDetection(); + + assertFalse(guardrail.validate(AiMessage.from("I apologize, but I cannot help with that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("Sorry, but I cannot assist.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I'm sorry, I cannot do that.")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("I'm afraid I cannot help.")).isSuccess()); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrailTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrailTest.java new file mode 100644 index 000000000000..ac7169842df8 --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/RegexPatternGuardrailTest.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.guardrail.InputGuardrailResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RegexPatternGuardrailTest { + + @Test + void testEmptyGuardrailAllowsAll() { + RegexPatternGuardrail guardrail = new RegexPatternGuardrail(); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("https://example.com")).isSuccess()); + } + + @Test + void testBlockingPattern() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.blocking( + "https?://[^\\s]+", "URLs are not allowed"); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Visit https://example.com")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Go to http://test.org")).isSuccess()); + } + + @Test + void testRequiringPattern() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.requiring( + "TICKET-\\d+", "Please include a ticket number"); + + assertTrue(guardrail.validate(UserMessage.from("Fix TICKET-123")).isSuccess()); + assertTrue(guardrail.validate(UserMessage.from("Working on TICKET-456 now")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + } + + @Test + void testMultipleDenyPatterns() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .denyPattern("https?://[^\\s]+", "URLs are not allowed") + .denyPattern("\\b(password|secret)\\b", "Sensitive keywords not allowed") + .build(); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Visit https://example.com")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("My password is 123")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("The secret is here")).isSuccess()); + } + + @Test + void testMultipleRequirePatterns() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .requirePattern("TICKET-\\d+", "Please include a ticket number") + .requirePattern("@\\w+", "Please mention a user") + .build(); + + assertTrue(guardrail.validate(UserMessage.from("TICKET-123 @john")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("TICKET-123")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("@john")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + } + + @Test + void testCombinedDenyAndRequire() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .denyPattern("https?://[^\\s]+", "URLs are not allowed") + .requirePattern("TICKET-\\d+", "Please include a ticket number") + .build(); + + assertTrue(guardrail.validate(UserMessage.from("Fix TICKET-123")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("TICKET-123 at https://example.com")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + } + + @Test + void testFailOnFirstMatch() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .denyPattern("pattern1", "Error 1") + .denyPattern("pattern2", "Error 2") + .failOnFirstMatch(true) + .build(); + + InputGuardrailResult result = guardrail.validate(UserMessage.from("Contains pattern1 and pattern2")); + assertFalse(result.isSuccess()); + // Should only contain first error due to failOnFirstMatch + assertTrue(result.toString().contains("Error 1")); + } + + @Test + void testCollectAllErrors() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .denyPattern("pattern1", "Error 1") + .denyPattern("pattern2", "Error 2") + .failOnFirstMatch(false) + .build(); + + InputGuardrailResult result = guardrail.validate(UserMessage.from("Contains pattern1 and pattern2")); + assertFalse(result.isSuccess()); + // Should contain both errors + assertTrue(result.toString().contains("Error 1")); + assertTrue(result.toString().contains("Error 2")); + } + + @Test + void testNullMessage() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.blocking("test", "Error"); + + InputGuardrailResult result = guardrail.validate((UserMessage) null); + assertTrue(result.isSuccess()); + } + + @Test + void testGetDenyPatterns() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .denyPattern("pattern1", "Error 1") + .denyPattern("pattern2", "Error 2") + .build(); + + assertEquals(2, guardrail.getDenyPatterns().size()); + } + + @Test + void testGetRequirePatterns() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.builder() + .requirePattern("pattern1", "Error 1") + .requirePattern("pattern2", "Error 2") + .build(); + + assertEquals(2, guardrail.getRequirePatterns().size()); + } + + @Test + void testCaseInsensitivePattern() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.blocking( + "(?i)blocked", "Blocked word found"); + + assertFalse(guardrail.validate(UserMessage.from("This is BLOCKED")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("This is blocked")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("This is Blocked")).isSuccess()); + } + + @Test + void testEmailPattern() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.blocking( + "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", "Email addresses not allowed"); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Contact me at [email protected]")).isSuccess()); + } + + @Test + void testPhoneNumberPattern() { + RegexPatternGuardrail guardrail = RegexPatternGuardrail.blocking( + "\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b", "Phone numbers not allowed"); + + assertTrue(guardrail.validate(UserMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Call me at 555-123-4567")).isSuccess()); + assertFalse(guardrail.validate(UserMessage.from("Call me at 5551234567")).isSuccess()); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrailTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrailTest.java new file mode 100644 index 000000000000..ceab1a54eb0a --- /dev/null +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/guardrails/WordCountGuardrailTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.agent.api.guardrails; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.guardrail.OutputGuardrailResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WordCountGuardrailTest { + + @Test + void testDefaultAllowsAll() { + WordCountGuardrail guardrail = new WordCountGuardrail(); + + assertTrue(guardrail.validate(AiMessage.from("Hello")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("Hello world how are you today")).isSuccess()); + } + + @Test + void testAtLeast() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(5); + + // Fewer than 5 words + assertFalse(guardrail.validate(AiMessage.from("Hello")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("Hello world")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("One two three four")).isSuccess()); + + // Exactly 5 words + assertTrue(guardrail.validate(AiMessage.from("One two three four five")).isSuccess()); + + // More than 5 words + assertTrue(guardrail.validate(AiMessage.from("One two three four five six seven")).isSuccess()); + } + + @Test + void testAtMost() { + WordCountGuardrail guardrail = WordCountGuardrail.atMost(5); + + // Fewer than 5 words + assertTrue(guardrail.validate(AiMessage.from("Hello")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("Hello world")).isSuccess()); + + // Exactly 5 words + assertTrue(guardrail.validate(AiMessage.from("One two three four five")).isSuccess()); + + // More than 5 words + assertFalse(guardrail.validate(AiMessage.from("One two three four five six")).isSuccess()); + } + + @Test + void testBetween() { + WordCountGuardrail guardrail = WordCountGuardrail.between(3, 7); + + // Fewer than 3 words + assertFalse(guardrail.validate(AiMessage.from("Hello")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("Hello world")).isSuccess()); + + // Between 3 and 7 words + assertTrue(guardrail.validate(AiMessage.from("One two three")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("One two three four five")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("One two three four five six seven")).isSuccess()); + + // More than 7 words + assertFalse(guardrail.validate(AiMessage.from("One two three four five six seven eight")).isSuccess()); + } + + @Test + void testBuilder() { + WordCountGuardrail guardrail = WordCountGuardrail.builder() + .minWords(10) + .maxWords(50) + .build(); + + assertEquals(10, guardrail.getMinWords()); + assertEquals(50, guardrail.getMaxWords()); + } + + @Test + void testNullMessage() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(1); + + OutputGuardrailResult result = guardrail.validate((AiMessage) null); + assertFalse(result.isSuccess()); + } + + @Test + void testEmptyMessage() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(1); + + assertFalse(guardrail.validate(AiMessage.from("")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from(" ")).isSuccess()); + } + + @Test + void testGetters() { + WordCountGuardrail guardrail = WordCountGuardrail.between(5, 100); + + assertEquals(5, guardrail.getMinWords()); + assertEquals(100, guardrail.getMaxWords()); + } + + @Test + void testWordCountWithPunctuation() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(5); + + // Words with punctuation should still be counted as words + assertTrue(guardrail.validate(AiMessage.from("Hello, world! How are you?")).isSuccess()); + } + + @Test + void testWordCountWithMultipleSpaces() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(3); + + // Multiple spaces should not create extra words + assertTrue(guardrail.validate(AiMessage.from("One two three")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("One two")).isSuccess()); + } + + @Test + void testWordCountWithNewlines() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(4); + + // Newlines should separate words + assertTrue(guardrail.validate(AiMessage.from("One\ntwo\nthree\nfour")).isSuccess()); + } + + @Test + void testWordCountWithTabs() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(3); + + // Tabs should separate words + assertTrue(guardrail.validate(AiMessage.from("One\ttwo\tthree")).isSuccess()); + } + + @Test + void testLongResponse() { + WordCountGuardrail guardrail = WordCountGuardrail.atMost(10); + + String longResponse + = "This is a very long response that contains many more words than the maximum allowed limit of ten words"; + assertFalse(guardrail.validate(AiMessage.from(longResponse)).isSuccess()); + } + + @Test + void testExactWordCount() { + WordCountGuardrail guardrail = WordCountGuardrail.between(5, 5); + + assertFalse(guardrail.validate(AiMessage.from("One two three four")).isSuccess()); + assertTrue(guardrail.validate(AiMessage.from("One two three four five")).isSuccess()); + assertFalse(guardrail.validate(AiMessage.from("One two three four five six")).isSuccess()); + } + + @Test + void testDefaultMaxIsDefault() { + WordCountGuardrail guardrail = WordCountGuardrail.atLeast(1); + + assertEquals(WordCountGuardrail.DEFAULT_MAX_WORDS, guardrail.getMaxWords()); + } + + @Test + void testDefaultMinIsDefault() { + WordCountGuardrail guardrail = WordCountGuardrail.atMost(100); + + assertEquals(WordCountGuardrail.DEFAULT_MIN_WORDS, guardrail.getMinWords()); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc index f67f50abab67..05eaec16a3cb 100644 --- a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc +++ b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc @@ -603,6 +603,15 @@ The `camel-langchain4j-agent-api` module provides production-ready guardrails in |`KeywordFilterGuardrail` |Blocks messages containing specific keywords or patterns. + +|`LanguageGuardrail` +|Validates the language/script of user messages. Can allow or block specific languages (English, Cyrillic, Chinese, Japanese, Korean, Arabic, Hebrew, Greek, Thai, Devanagari). + +|`CodeInjectionGuardrail` +|Detects potential code injection attempts including shell commands, SQL injection, JavaScript, HTML/XSS, path traversal, command chaining, and template injection. + +|`RegexPatternGuardrail` +|Flexible guardrail using custom regex patterns. Can define deny patterns (block if matched) and require patterns (must be present). |=== ===== Available Output Guardrails @@ -622,6 +631,12 @@ The `camel-langchain4j-agent-api` module provides production-ready guardrails in |`KeywordOutputFilterGuardrail` |Blocks or redacts specific content in AI responses. + +|`NotEmptyGuardrail` +|Ensures AI responses are not empty or contain only whitespace. Can optionally detect refusal patterns (e.g., "I cannot", "I'm unable to"). + +|`WordCountGuardrail` +|Validates the word count of AI responses. Can enforce minimum, maximum, or range constraints. |=== ==== Quick Start with Default Guardrails @@ -814,6 +829,122 @@ KeywordOutputFilterGuardrail outputFilter = KeywordOutputFilterGuardrail.builder .build(); ---- +===== Language Validation Configuration + +[source,java] +---- +import org.apache.camel.component.langchain4j.agent.api.guardrails.LanguageGuardrail; +import org.apache.camel.component.langchain4j.agent.api.guardrails.LanguageGuardrail.Language; + +// Allow only English input +LanguageGuardrail englishOnly = LanguageGuardrail.allowOnly(Language.ENGLISH); + +// Allow English and Latin script languages (Spanish, French, German, etc.) +LanguageGuardrail latinLanguages = LanguageGuardrail.allowOnly( + Language.ENGLISH, Language.LATIN_SCRIPT); + +// Block specific languages +LanguageGuardrail blockCyrillic = LanguageGuardrail.block(Language.CYRILLIC); + +// Custom configuration with mixed content control +LanguageGuardrail customLanguage = LanguageGuardrail.builder() + .allowedLanguages(Language.ENGLISH, Language.LATIN_SCRIPT) + .blockedLanguages(Language.CYRILLIC) + .allowMixed(false) // Don't allow mixed language content + .build(); +---- + +===== Code Injection Detection Configuration + +[source,java] +---- +import org.apache.camel.component.langchain4j.agent.api.guardrails.CodeInjectionGuardrail; +import org.apache.camel.component.langchain4j.agent.api.guardrails.CodeInjectionGuardrail.InjectionType; + +// Default: detect all code injection types +CodeInjectionGuardrail defaultGuard = new CodeInjectionGuardrail(); + +// Strict mode: fail on any single pattern match +CodeInjectionGuardrail strictGuard = CodeInjectionGuardrail.strict(); + +// Detect only specific injection types +CodeInjectionGuardrail sqlAndShellOnly = CodeInjectionGuardrail.forTypes( + InjectionType.SQL_INJECTION, InjectionType.SHELL_COMMAND); + +// Custom configuration +CodeInjectionGuardrail customGuard = CodeInjectionGuardrail.builder() + .detectTypes(InjectionType.SQL_INJECTION, InjectionType.JAVASCRIPT, InjectionType.PATH_TRAVERSAL) + .strict(true) + .build(); +---- + +===== Regex Pattern Guardrail Configuration + +[source,java] +---- +import org.apache.camel.component.langchain4j.agent.api.guardrails.RegexPatternGuardrail; + +// Block messages containing URLs +RegexPatternGuardrail noUrls = RegexPatternGuardrail.blocking( + "https?://[^\\s]+", "URLs are not allowed in messages"); + +// Require messages to contain a ticket number +RegexPatternGuardrail requireTicket = RegexPatternGuardrail.requiring( + "TICKET-\\d+", "Please include a ticket number (e.g., TICKET-123)"); + +// Complex configuration with multiple patterns +RegexPatternGuardrail customPatterns = RegexPatternGuardrail.builder() + .denyPattern("https?://[^\\s]+", "URLs are not allowed") + .denyPattern("\\b(password|secret)\\b", "Sensitive keywords are not allowed") + .requirePattern("[A-Z]{2,4}-\\d+", "Please include a valid issue ID") + .failOnFirstMatch(true) // Stop checking after first failure + .build(); +---- + +===== Not Empty Guardrail Configuration + +[source,java] +---- +import org.apache.camel.component.langchain4j.agent.api.guardrails.NotEmptyGuardrail; + +// Default: just ensure response is not empty +NotEmptyGuardrail notEmpty = new NotEmptyGuardrail(); + +// Also detect refusal patterns like "I cannot", "I'm unable to" +NotEmptyGuardrail withRefusalDetection = NotEmptyGuardrail.withRefusalDetection(); + +// Require minimum meaningful length +NotEmptyGuardrail minLength = NotEmptyGuardrail.withMinLength(50); + +// Custom configuration +NotEmptyGuardrail customNotEmpty = new NotEmptyGuardrail( + true, // detectRefusals + 100 // minMeaningfulLength +); +---- + +===== Word Count Guardrail Configuration + +[source,java] +---- +import org.apache.camel.component.langchain4j.agent.api.guardrails.WordCountGuardrail; + +// Require at least 10 words +WordCountGuardrail atLeast10 = WordCountGuardrail.atLeast(10); + +// Limit to maximum 500 words +WordCountGuardrail atMost500 = WordCountGuardrail.atMost(500); + +// Require between 50 and 200 words +WordCountGuardrail between = WordCountGuardrail.between(50, 200); + +// Custom configuration +WordCountGuardrail custom = WordCountGuardrail.builder() + .minWords(20) + .maxWords(1000) + .build(); +---- + ==== Complete Example with Memory and Guardrails [source,java] @@ -912,6 +1043,18 @@ public class AgentConfig { |Blocked keyword found |Blocks the request +|`LanguageGuardrail` +|Disallowed language/script detected +|Blocks the request + +|`CodeInjectionGuardrail` +|Code injection pattern detected +|Blocks the request + +|`RegexPatternGuardrail` +|Deny pattern matched or require pattern missing +|Blocks the request + |`OutputLengthGuardrail` |Response too short/long |Retries LLM or truncates @@ -927,6 +1070,14 @@ public class AgentConfig { |`KeywordOutputFilterGuardrail` |Blocked content in response |Blocks or redacts + +|`NotEmptyGuardrail` +|Empty response or refusal detected +|Retries LLM call + +|`WordCountGuardrail` +|Word count outside allowed range +|Retries LLM call |=== === Multimodal Content Support
