refactor(agent): replace SkillToolManifest with SkillToolResolver and relocate test

- Switch SkillBoxFactory to SkillToolResolver, which resolves tool classes from
  the framework container (enabling DI) and falls back to no-arg reflection,
  resolving tools per AgentSkill instead of by name.
- Remove the obsolete SkillToolManifest.
- Move ReActAgentComponentTest from liteflow-react-agent-core to
  liteflow-testcase-el per the test-placement convention.
- Drop the temporary surefire skipTests=false override in react-agent-core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-05-31 19:29:49 +08:00
parent ec5562c45f
commit 115ffd652c
4 changed files with 13 additions and 193 deletions

View File

@@ -45,16 +45,4 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -48,7 +48,7 @@ public final class SkillBoxFactory {
FileSystemSkillRepository repository = new FileSystemSkillRepository(root);
List<AgentSkill> allSkills = repository.getAllSkills();
List<AgentSkill> selected = selectSkills(allSkills, allowedSkills, skillsConfig);
SkillToolManifest manifest = new SkillToolManifest(root, skillsConfig);
SkillToolResolver toolResolver = new SkillToolResolver(skillsConfig);
SkillBox skillBox = createSkillBox(toolkit, workspaceDir);
Map<String, String> skillIdToName = new LinkedHashMap<>();
List<String> skillNames = new ArrayList<>();
@@ -56,7 +56,7 @@ public final class SkillBoxFactory {
for (AgentSkill skill : selected) {
skillIdToName.put(skill.getSkillId(), skill.getName());
skillNames.add(skill.getName());
List<Object> skillTools = manifest.instantiateTools(skill.getName());
List<Object> skillTools = toolResolver.instantiateTools(skill);
if (skillTools.isEmpty()) {
skillBox.registerSkill(skill);
} else {

View File

@@ -1,175 +0,0 @@
package com.yomahub.liteflow.agent.skill;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.SkillsConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class SkillToolManifest {
private static final Logger LOG = LoggerFactory.getLogger(SkillToolManifest.class);
private final SkillsConfig config;
private final Map<String, List<Class<?>>> toolClasses = new LinkedHashMap<>();
public SkillToolManifest(Path skillsRoot, SkillsConfig config) {
this.config = config;
scan(skillsRoot);
}
public List<Object> instantiateTools(String skillName) {
List<Class<?>> classes = toolClasses.get(skillName);
if (classes == null || classes.isEmpty()) {
return List.of();
}
List<Object> instances = new ArrayList<>(classes.size());
for (Class<?> clazz : classes) {
try {
instances.add(clazz.getDeclaredConstructor().newInstance());
} catch (ReflectiveOperationException e) {
handleProblem("Skill '" + skillName + "' tool class '" + clazz.getName()
+ "' instantiation failed", e);
}
}
return List.copyOf(instances);
}
private void scan(Path skillsRoot) {
if (!Files.isDirectory(skillsRoot)) {
handleProblem("Skills root not found: " + skillsRoot, null);
return;
}
try (Stream<Path> dirs = Files.list(skillsRoot)) {
dirs.filter(Files::isDirectory)
.sorted()
.forEach(this::loadOne);
} catch (IOException e) {
handleProblem("Failed to scan skills dir: " + skillsRoot, e);
}
}
private void loadOne(Path skillDir) {
Path skillMd = skillDir.resolve("SKILL.md");
if (!Files.exists(skillMd)) {
handleProblem("Skill file not found: " + skillMd, null);
return;
}
try {
Map<String, Object> frontmatter = parseFrontmatter(Files.readString(skillMd));
Object nameObj = frontmatter.get("name");
if (nameObj == null) {
return;
}
Object toolsObj = frontmatter.get("tools");
if (toolsObj == null) {
return;
}
String skillName = nameObj.toString().trim();
List<Class<?>> resolved = new ArrayList<>();
for (String className : toClassNameList(toolsObj)) {
try {
resolved.add(Class.forName(className));
} catch (ClassNotFoundException e) {
handleProblem("Skill '" + skillName + "' references unknown tool class '"
+ className + "'", e);
}
}
if (!resolved.isEmpty()) {
toolClasses.put(skillName, List.copyOf(resolved));
LOG.info("Skill '{}' bound to tool classes: {}", skillName,
resolved.stream().map(Class::getName).toList());
}
} catch (IOException e) {
handleProblem("Failed to read skill file: " + skillMd, e);
}
}
static Map<String, Object> parseFrontmatter(String content) {
Map<String, Object> result = new LinkedHashMap<>();
if (content == null || !content.startsWith("---")) {
return result;
}
int end = content.indexOf("\n---", 3);
if (end < 0) {
return result;
}
String[] lines = content.substring(3, end).split("\\R");
String currentListKey = null;
List<String> currentList = null;
for (String rawLine : lines) {
String line = rawLine.stripTrailing();
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
continue;
}
if (currentListKey != null && trimmed.startsWith("-")) {
currentList.add(unquote(trimmed.substring(1).trim()));
continue;
}
currentListKey = null;
currentList = null;
int colon = trimmed.indexOf(':');
if (colon < 0) {
continue;
}
String key = trimmed.substring(0, colon).trim();
String value = trimmed.substring(colon + 1).trim();
if (value.isEmpty()) {
currentListKey = key;
currentList = new ArrayList<>();
result.put(key, currentList);
} else {
result.put(key, unquote(value));
}
}
return result;
}
private List<String> toClassNameList(Object field) {
if (field instanceof List<?> list) {
return list.stream()
.map(Object::toString)
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
return Stream.of(field.toString().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
private static String unquote(String value) {
if (value.length() >= 2) {
char first = value.charAt(0);
char last = value.charAt(value.length() - 1);
if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
return value.substring(1, value.length() - 1);
}
}
return value;
}
private void handleProblem(String message, Exception e) {
if (config.isStrict()) {
if (e == null) {
throw new AgentConfigException(message);
}
throw new AgentConfigException(message, e);
}
if (e == null) {
LOG.warn(message);
} else {
LOG.warn("{}: {}", message, e.getMessage());
}
}
}

View File

@@ -1,7 +1,10 @@
package com.yomahub.liteflow.agent.component;
package com.yomahub.liteflow.test.agent.unit;
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
import com.yomahub.liteflow.agent.component.ReActAgentContext;
import com.yomahub.liteflow.agent.model.ModelSpec;
import com.yomahub.liteflow.slot.Slot;
import io.agentscope.core.message.Msg;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
@@ -11,13 +14,13 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ReActAgentComponentTest {
public class ReActAgentComponentTest {
@Test
void effectiveSystemPromptPrependsFrameworkPromptAndAppendsComponentPrompt() {
TestAgentComponent component = new TestAgentComponent();
String prompt = component.effectiveSystemPrompt();
String prompt = component.effectiveSystemPromptForTest();
assertTrue(prompt.contains("使用用户提问所用的语言"));
assertTrue(prompt.contains("每次调用工具前"));
@@ -71,7 +74,11 @@ class ReActAgentComponentTest {
return "hello";
}
void handleReplyForTest(io.agentscope.core.message.Msg reply) {
String effectiveSystemPromptForTest() {
return effectiveSystemPrompt();
}
void handleReplyForTest(Msg reply) {
handleReply(reply);
}
}