Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion ali-agentic-adk-java/ali-agentic-adk-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Context for @Component/@Aspect support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
<scope>provided</scope>
</dependency>
<!-- Spring AOP + AspectJ weaving for moderation aspect -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${springframework.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.22.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
Expand Down Expand Up @@ -237,4 +257,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.alibaba.agentic.core.moderation;

/**
* Moderation action to take when content hits a rule.
*/
public enum ModerationAction {
ALLOW,
MASK,
BLOCK
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.alibaba.agentic.core.moderation;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.Collections;
import java.util.List;

@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class ModerationDecision {
private ModerationAction action; // ALLOW | MASK | BLOCK
private List<String> categories; // matched categories/keywords
private String reason; // human-readable reason

public static ModerationDecision allow() {
return new ModerationDecision(ModerationAction.ALLOW, Collections.emptyList(), null);
}

public static ModerationDecision mask(List<String> categories, String reason) {
return new ModerationDecision(ModerationAction.MASK, categories, reason);
}

public static ModerationDecision block(List<String> categories, String reason) {
return new ModerationDecision(ModerationAction.BLOCK, categories, reason);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.alibaba.agentic.core.moderation;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class ModerationProperties {
/** enable moderation globally */
private boolean enabled = false;
/** observe-only mode: log hits but don't block */
private boolean observeOnly = true;
/** default action when rules hit (if provider doesn't give one) */
private ModerationAction defaultAction = ModerationAction.MASK;
/** optional external blocklist path (file system) */
private String blocklistPath;
/** timeout for provider checks (ms) - for future async providers */
private int timeoutMillis = 500; // not used in rule-based

public static ModerationProperties fromEnv() {
ModerationProperties p = new ModerationProperties();
String enabled = getenv("ADK_MODERATION_ENABLED");
String observe = getenv("ADK_MODERATION_OBSERVE_ONLY");
String action = getenv("ADK_MODERATION_DEFAULT_ACTION");
String blocklist = getenv("ADK_MODERATION_BLOCKLIST_PATH");
String timeout = getenv("ADK_MODERATION_TIMEOUT_MS");

if (enabled != null) p.setEnabled("true".equalsIgnoreCase(enabled));
if (observe != null) p.setObserveOnly("true".equalsIgnoreCase(observe));
if (action != null) {
try {
p.setDefaultAction(ModerationAction.valueOf(action.trim().toUpperCase()));
} catch (Exception ignored) {}
}
if (blocklist != null && !blocklist.isBlank()) p.setBlocklistPath(blocklist);
if (timeout != null) {
try { p.setTimeoutMillis(Integer.parseInt(timeout)); } catch (Exception ignored) {}
}
return p;
}

private static String getenv(String key) {
String v = System.getProperty(key);
if (v == null) v = System.getenv(key);
return v;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.alibaba.agentic.core.moderation;

import com.alibaba.agentic.core.engine.delegation.domain.LlmRequest;

/**
* Provider abstraction for content moderation.
*/
public interface ModerationProvider {

/**
* Check the incoming prompt (messages) before sending to the model.
*/
ModerationDecision checkPrompt(LlmRequest request);

/**
* Check a single output text chunk from the model (streamed).
*/
ModerationDecision checkChunk(String text);

/**
* Check the final aggregated output text.
*/
ModerationDecision checkFinal(String text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.alibaba.agentic.core.moderation;

import com.alibaba.agentic.core.engine.delegation.domain.LlmRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ModerationService {

private final ModerationProvider provider;
private final RuleBasedModerationProvider defaultProvider;

private final ModerationProperties properties;

public ModerationService() {
// env-based defaults when DI is not yet in effect
this.properties = ModerationProperties.fromEnv();
this.defaultProvider = new RuleBasedModerationProvider(this.properties);
this.provider = this.defaultProvider;
}

@Autowired(required = false)
public ModerationService(ModerationProvider provider,
RuleBasedModerationProvider defaultProvider) {
this.properties = ModerationProperties.fromEnv();
this.provider = provider != null ? provider : defaultProvider;
this.defaultProvider = defaultProvider;
}

public boolean isEnabled() {
return properties.isEnabled();
}

public boolean isObserveOnly() {
return properties.isObserveOnly();
}

public ModerationDecision checkPrompt(LlmRequest req) {
return provider.checkPrompt(req);
}

public ModerationDecision checkChunk(String text) {
return provider.checkChunk(text);
}

public ModerationDecision checkFinal(String text) {
return provider.checkFinal(text);
}

public String mask(String text) {
if (provider instanceof RuleBasedModerationProvider) {
return ((RuleBasedModerationProvider) provider).mask(text);
}
return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.alibaba.agentic.core.moderation;

import com.alibaba.agentic.core.engine.delegation.domain.LlmRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

@Component
@Slf4j
public class RuleBasedModerationProvider implements ModerationProvider {

private final ModerationProperties properties;
private Set<String> blockWords = new HashSet<>();

public RuleBasedModerationProvider() {
// fallback to env-based when not wired via DI
this.properties = ModerationProperties.fromEnv();
}

public RuleBasedModerationProvider(ModerationProperties properties) {
this.properties = properties;
}

@PostConstruct
public void init() {
loadBlocklist();
}

private void loadBlocklist() {
List<String> lines = new ArrayList<>();
// 1) optional external file
if (properties.getBlocklistPath() != null) {
File f = new File(properties.getBlocklistPath());
if (f.exists() && f.isFile()) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(f), StandardCharsets.UTF_8))) {
lines.addAll(br.lines().collect(Collectors.toList()));
} catch (Exception e) {
log.warn("Failed to load external blocklist: {}", f.getAbsolutePath(), e);
}
}
}
// 2) classpath default
try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("moderation/blocklist.txt")) {
if (is != null) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
lines.addAll(br.lines().collect(Collectors.toList()));
}
}
} catch (Exception e) {
log.debug("No default blocklist found in classpath.");
}
// normalize
blockWords = lines.stream()
.map(String::trim)
.filter(s -> !s.isBlank() && !s.startsWith("#"))
.collect(Collectors.toSet());
log.info("RuleBasedModerationProvider loaded block words: {}", blockWords.size());
}

@Override
public ModerationDecision checkPrompt(LlmRequest request) {
if (request == null || request.getMessages() == null) {
return ModerationDecision.allow();
}
String text = request.getMessages().stream()
.filter(Objects::nonNull)
.map(LlmRequest.Message::getContent)
.filter(Objects::nonNull)
.collect(Collectors.joining("\n"));
return decide(text);
}

@Override
public ModerationDecision checkChunk(String text) {
return decide(text);
}

@Override
public ModerationDecision checkFinal(String text) {
return decide(text);
}

private ModerationDecision decide(String text) {
if (text == null || text.isBlank()) return ModerationDecision.allow();
List<String> hits = new ArrayList<>();
String lower = text.toLowerCase(Locale.ROOT);
for (String w : blockWords) {
if (w.isEmpty()) continue;
if (lower.contains(w.toLowerCase(Locale.ROOT))) {
hits.add(w);
}
}
if (hits.isEmpty()) return ModerationDecision.allow();

// default behavior: use configured defaultAction
ModerationAction action = properties.getDefaultAction() == null ? ModerationAction.MASK : properties.getDefaultAction();
String reason = "Matched blocked words: " + String.join(", ", hits);
if (action == ModerationAction.BLOCK) return ModerationDecision.block(hits, reason);
if (action == ModerationAction.MASK) return ModerationDecision.mask(hits, reason);
return ModerationDecision.allow();
}

public String mask(String text) {
if (text == null || text.isBlank()) return text;
String result = text;
for (String w : blockWords) {
if (w.isEmpty()) continue;
String pattern = java.util.regex.Pattern.quote(w);
result = result.replaceAll("(?i)" + pattern, repeat('*', Math.min(w.length(), 4)));
}
return result;
}

private static String repeat(char c, int count) {
char[] arr = new char[count];
Arrays.fill(arr, c);
return new String(arr);
}

public ModerationProperties properties() {
return properties;
}
}
Loading