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 3664491920aaf341a43e0d31c927b5758f527bf3 Author: Andrea Cosentino <[email protected]> AuthorDate: Fri Dec 19 10:53:39 2025 +0100 Added more Langchain4j Agent 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 | 176 +++++++++++ 6 files changed, 1350 insertions(+) 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..4a968fda29be --- /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,176 @@ +/* + * 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); + } + + @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; + } +}
