Skip to content

Commit 99079b0

Browse files
committed
fix
1 parent 88ec2e6 commit 99079b0

File tree

8 files changed

+115
-345
lines changed

8 files changed

+115
-345
lines changed

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/skills/SkillMetadata.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* Metadata for a Claude-style Skill.
2626
*
27-
* A Skill is a reusable package of instructions and context that extends Claude's capabilities.
27+
* A Skill is a reusable package of instructions and context that extends the LLM's capabilities.
2828
* Skills are automatically discovered and used by the LLM when relevant to the user's request.
2929
*/
3030
public class SkillMetadata {
@@ -88,9 +88,9 @@ public void setModel(String model) {
8888

8989
/**
9090
* Load the full content of the SKILL.md file.
91-
* The content is cached after the first load.
91+
* The content is cached after the first load (lazy loading).
9292
*
93-
* @return the full content of the skill (without frontmatter)
93+
* @return the full content of SKILL.md (without frontmatter)
9494
* @throws IOException if the skill file cannot be read
9595
*/
9696
public String loadFullContent() throws IOException {

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/skills/SkillRegistry.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ public void clear() {
114114
logger.debug("Cleared all skills");
115115
}
116116

117+
/**
118+
* Unregister a skill by name.
119+
* Package-private: Only used internally by SkillsHook.
120+
*
121+
* @param name the skill name to unregister
122+
* @return true if the skill was removed, false if it didn't exist
123+
*/
124+
boolean unregister(String name) {
125+
if (name == null || name.isEmpty()) {
126+
throw new IllegalArgumentException("Skill name cannot be null or empty");
127+
}
128+
129+
SkillMetadata removed = skills.remove(name);
130+
if (removed != null) {
131+
logger.info("Unregistered skill: {}", name);
132+
return true;
133+
} else {
134+
logger.debug("Attempted to unregister non-existent skill: {}", name);
135+
return false;
136+
}
137+
}
138+
117139
/**
118140
* Match skills based on user request.
119141
*
@@ -170,7 +192,7 @@ private boolean isStopWord(String word) {
170192
/**
171193
* Generate a prompt describing all available skills.
172194
* This prompt is injected into the LLM's context to enable skill discovery.
173-
*
195+
*
174196
* @return the skills list prompt
175197
*/
176198
public String generateSkillsListPrompt() {

spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/skills/SkillsHook.java

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,10 @@ public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, Ru
107107

108108
List<Message> newMessages = new ArrayList<>(messages);
109109

110-
// Check if this is the first call for this thread
111110
boolean isFirstCall = !skillsListInjectedPerThread.getOrDefault(threadId, false);
112-
113-
// Get user request for skill matching
111+
114112
String userRequest = extractLastUserMessage(messages);
115-
116-
// Match relevant skills
113+
117114
List<SkillMetadata> matchedSkills = new ArrayList<>();
118115
Set<String> loadedSkills = loadedSkillsPerThread.computeIfAbsent(
119116
threadId, k -> new HashSet<>());
@@ -124,38 +121,33 @@ public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, Ru
124121
.toList();
125122
}
126123

127-
// Only inject if it's first call OR there are new matched skills
128124
if (isFirstCall || !matchedSkills.isEmpty()) {
129125
try {
130-
// Build complete system prompt (single message)
131126
String systemPrompt = buildSystemPrompt(isFirstCall, matchedSkills);
132127

133128
if (!systemPrompt.isEmpty()) {
134129
SystemMessage skillsMessage = new SystemMessage(systemPrompt);
135-
136-
// Insert at the beginning (after any existing system messages)
130+
137131
int insertIndex = findSystemMessageInsertIndex(newMessages);
138132
newMessages.add(insertIndex, skillsMessage);
139-
140-
// Mark as injected
133+
141134
if (isFirstCall) {
142135
skillsListInjectedPerThread.put(threadId, true);
143-
logger.debug("[Thread {}] Injected skills overview with {} skills",
136+
logger.debug("Thread {} Injected skills overview with {} skills",
144137
threadId, skillRegistry.size());
145138
}
146-
147-
// Mark matched skills as loaded
139+
148140
for (SkillMetadata skill : matchedSkills) {
149141
loadedSkills.add(skill.getName());
150-
logger.info("[Thread {}] Activated skill '{}'", threadId, skill.getName());
142+
logger.info("Thread {} Activated skill '{}'", threadId, skill.getName());
151143
}
152144

153145
Map<String, Object> update = new HashMap<>();
154146
update.put("messages", newMessages);
155147
return CompletableFuture.completedFuture(update);
156148
}
157149
} catch (Exception e) {
158-
logger.error("[Thread {}] Failed to inject skills: {}",
150+
logger.error("Thread {} Failed to inject skills: {}",
159151
threadId, e.getMessage(), e);
160152
}
161153
}
@@ -165,9 +157,8 @@ public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, Ru
165157

166158
@Override
167159
public Map<String, KeyStrategy> getKeyStrategys() {
168-
// No custom key strategies needed
169-
return Map.of();
170-
}
160+
return super.getKeyStrategys();
161+
}
171162

172163
/**
173164
* Extract messages from state.
@@ -203,17 +194,9 @@ private String extractLastUserMessage(List<Message> messages) {
203194
private String buildSystemPrompt(boolean includeOverview, List<SkillMetadata> matchedSkills) {
204195
StringBuilder prompt = new StringBuilder();
205196

206-
// Part 1: Skills overview (only on first call)
207197
if (includeOverview && skillRegistry.size() > 0) {
208-
prompt.append("You are an AI assistant with access to specialized skills.\n");
209-
prompt.append("When a user's request aligns with a skill's purpose, you should apply that skill's instructions.\n\n");
210-
prompt.append("Available Skills:\n");
211-
212-
for (SkillMetadata skill : skillRegistry.listAll()) {
213-
prompt.append(String.format("- **%s**: %s\n", skill.getName(), skill.getDescription()));
214-
}
215-
216-
prompt.append("\n");
198+
String skillsListPrompt = skillRegistry.generateSkillsListPrompt();
199+
prompt.append(skillsListPrompt);
217200
}
218201

219202
// Part 2: Activated skills (full content)
@@ -274,6 +257,81 @@ public boolean hasSkill(String skillName) {
274257
return skillRegistry.contains(skillName);
275258
}
276259

260+
/**
261+
* Load a skill from a directory at runtime.
262+
* This allows dynamically adding skills without restarting the application.
263+
*
264+
* @param skillDirectory the directory containing SKILL.md
265+
* @return true if the skill was loaded successfully
266+
*/
267+
public boolean loadSkill(String skillDirectory) {
268+
try {
269+
SkillScanner scanner = new SkillScanner();
270+
SkillMetadata skill = scanner.loadSkill(java.nio.file.Path.of(skillDirectory));
271+
272+
if (skill != null) {
273+
skillRegistry.register(skill);
274+
logger.info("Loaded skill '{}' from {}", skill.getName(), skillDirectory);
275+
return true;
276+
} else {
277+
logger.warn("Failed to load skill from {}", skillDirectory);
278+
return false;
279+
}
280+
} catch (Exception e) {
281+
logger.error("Error loading skill from {}: {}", skillDirectory, e.getMessage(), e);
282+
return false;
283+
}
284+
}
285+
286+
/**
287+
* Unload a skill at runtime.
288+
* This removes the skill from the registry.
289+
*
290+
* <p>Example usage:
291+
* <pre>{@code
292+
* hook.unloadSkill("pdf-extractor");
293+
* }</pre>
294+
*
295+
* @param skillName the name of the skill to unload
296+
* @return true if the skill was unloaded successfully
297+
*/
298+
public boolean unloadSkill(String skillName) {
299+
boolean removed = skillRegistry.unregister(skillName);
300+
if (removed) {
301+
logger.info("Unloaded skill '{}'", skillName);
302+
}
303+
return removed;
304+
}
305+
306+
/**
307+
* Reload a skill at runtime.
308+
* This unloads the existing skill and loads it again from disk.
309+
* Useful for updating skill definitions without restarting.
310+
*
311+
* <p>Example usage:
312+
* <pre>{@code
313+
* hook.reloadSkill("pdf-extractor", "./skills/pdf-extractor");
314+
* }</pre>
315+
*
316+
* @param skillName the name of the skill to reload
317+
* @param skillDirectory the directory containing SKILL.md
318+
* @return true if the skill was reloaded successfully
319+
*/
320+
public boolean reloadSkill(String skillName, String skillDirectory) {
321+
logger.info("Reloading skill '{}'", skillName);
322+
unloadSkill(skillName);
323+
return loadSkill(skillDirectory);
324+
}
325+
326+
/**
327+
* Get all registered skills.
328+
*
329+
* @return list of all skill metadata
330+
*/
331+
public List<SkillMetadata> listSkills() {
332+
return skillRegistry.listAll();
333+
}
334+
277335
/**
278336
* Get all required tools from registered skills.
279337
* This method analyzes all skills' allowed-tools and returns a set of tool names.

spring-ai-alibaba-agent-framework/src/test/java/com/alibaba/cloud/ai/graph/agent/hooks/skills/SkillsHookTest.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ void testSkillsHookIntegrationWithScriptExecution(@TempDir Path tempDir) throws
9595
.model(chatModel)
9696
.saver(new MemorySaver())
9797
.hooks(
98-
hook, // SkillsHook
99-
// ShellToolAgentHook 会自动从 tools 中提取 ShellTool 并管理其生命周期
98+
hook,
10099
ShellToolAgentHook.builder()
101100
.shellToolName("shell") // 匹配工具名称
102101
.build()
@@ -169,14 +168,5 @@ void testSkillsHookIntegrationWithScriptExecution(@TempDir Path tempDir) throws
169168
System.out.println(" [" + i + "] " + msgType + ": " +
170169
preview.replace("\n", " ") + (msg.getText().length() > 200 ? "..." : ""));
171170
}
172-
173-
System.out.println("\n集成测试通过!");
174-
System.out.println("Skills 自动加载");
175-
System.out.println("工具自动创建");
176-
System.out.println("Skills 指令注入(单条 SystemMess age)");
177-
System.out.println("ReactAgent 正常工作");
178-
if (hasToolResponse) {
179-
System.out.println(" - 工具调用成功");
180-
}
181171
}
182172
}

spring-ai-alibaba-agent-framework/src/test/resources/skills/code-reviewer/SKILL.md

Lines changed: 0 additions & 114 deletions
This file was deleted.

0 commit comments

Comments
 (0)