mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(skill): boost newly installed skills + teach LLM to widen the catalog search
This commit is contained in:
parent
0b321dc903
commit
5789a28e19
@ -335,6 +335,7 @@ public class SkillPackageResolver {
|
||||
.enabled(Boolean.TRUE.equals(entity.getEnabled()))
|
||||
.icon(entity.getIcon())
|
||||
.builtin(Boolean.TRUE.equals(entity.getBuiltin()))
|
||||
.createTime(entity.getCreateTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -375,6 +376,7 @@ public class SkillPackageResolver {
|
||||
.enabled(Boolean.TRUE.equals(entity.getEnabled()))
|
||||
.icon(entity.getIcon())
|
||||
.builtin(Boolean.TRUE.equals(entity.getBuiltin()))
|
||||
.createTime(entity.getCreateTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@ -395,10 +395,17 @@ public class SkillRuntimeService {
|
||||
int descLimit = promptDescriptionLimit(maxInputTokens);
|
||||
Set<String> recentNames = usageService.recentLoadedSkillNames(agentId, 8);
|
||||
Set<String> frequentNames = usageService.frequentlyLoadedSkillNames(8);
|
||||
// Boost freshly installed skills for a short window so a skill the
|
||||
// user *just* added is visible in the compact catalog before it has
|
||||
// any usage history. Without this, qwen-turbo-style 8-entry budgets
|
||||
// hide new skills behind 40+ existing ones, and the LLM tells the
|
||||
// user "no such skill" minutes after they uploaded it.
|
||||
java.time.LocalDateTime recencyCutoff = java.time.LocalDateTime.now().minus(NEW_SKILL_BOOST_WINDOW);
|
||||
List<ResolvedSkill> sorted = SkillCatalogSorter.sortResolved(visibleSkills, SkillCatalogSort.RECOMMENDED)
|
||||
.stream()
|
||||
.sorted(java.util.Comparator
|
||||
.comparingInt((ResolvedSkill s) -> recentNames.contains(s.getName()) ? 0 : 1)
|
||||
.comparingInt((ResolvedSkill s) -> isRecentlyInstalled(s, recencyCutoff) ? 0 : 1)
|
||||
.thenComparingInt(s -> recentNames.contains(s.getName()) ? 0 : 1)
|
||||
.thenComparingInt(s -> frequentNames.contains(s.getName()) ? 0 : 1)
|
||||
.thenComparing(SkillCatalogSorter.resolvedComparator(SkillCatalogSort.RECOMMENDED)))
|
||||
.toList();
|
||||
@ -416,7 +423,12 @@ public class SkillRuntimeService {
|
||||
sb.append("\n\n## Skills\n");
|
||||
sb.append("This is a compact catalog. If a listed skill matches the task, ");
|
||||
sb.append("first call `readSkillFile(skillName=<name>, filePath=\"SKILL.md\")` and follow its instructions. ");
|
||||
sb.append("If none of these skills match, call `listAvailableSkills()` to inspect the broader catalog. ");
|
||||
sb.append("If none of these skills match, call `listAvailableSkills()` to inspect the broader catalog ");
|
||||
sb.append("(it accepts `keyword=<part of name>` and `limit=` up to 50 — use them to search by topic ");
|
||||
sb.append("when the default page is truncated). ");
|
||||
sb.append("If the user names a specific skill that isn't in this table, ");
|
||||
sb.append("call `readSkillFile(skillName=\"<exact-name>\", filePath=\"SKILL.md\")` directly — ");
|
||||
sb.append("the catalog above is intentionally compact and doesn't list every active skill. ");
|
||||
sb.append("Skills are documentation packages — calling a skill name as a tool will fail. ");
|
||||
sb.append("Skills with a `scripts/` directory expose `runSkillScript`; SKILL.md will name the script when needed.\n\n");
|
||||
sb.append("| Skill | Status | Description |\n");
|
||||
@ -458,6 +470,28 @@ public class SkillRuntimeService {
|
||||
return tools == null || tools.isEmpty() || effectiveToolNames.containsAll(tools);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat skills installed within this window as "new" for the prompt
|
||||
* catalog ranker. Long enough that a user who installs on Friday and
|
||||
* comes back Monday still sees the boost; short enough that the
|
||||
* catalog reverts to usage-based ordering before the boost slot
|
||||
* crowds out genuinely useful skills.
|
||||
*/
|
||||
public static final java.time.Duration NEW_SKILL_BOOST_WINDOW = java.time.Duration.ofDays(7);
|
||||
|
||||
/**
|
||||
* Returns true if the skill's row was created after {@code cutoff}.
|
||||
* Builtins and virtual MCP/ACP skills typically have no createTime;
|
||||
* they are not boosted (the user didn't just install them). Public so
|
||||
* the user-facing {@code listAvailableSkills} catalog can apply the
|
||||
* same boost as the prompt enhancement.
|
||||
*/
|
||||
public static boolean isRecentlyInstalled(ResolvedSkill skill, java.time.LocalDateTime cutoff) {
|
||||
if (skill == null || skill.getCreateTime() == null) return false;
|
||||
if (skill.isBuiltin()) return false;
|
||||
return skill.getCreateTime().isAfter(cutoff);
|
||||
}
|
||||
|
||||
private static int promptCatalogEntryLimit(Integer maxInputTokens) {
|
||||
int max = maxInputTokens != null && maxInputTokens > 0 ? maxInputTokens : 8192;
|
||||
if (max <= 8192) return 8;
|
||||
|
||||
@ -6,6 +6,7 @@ import lombok.Data;
|
||||
import vip.mate.skill.manifest.SkillManifest;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -72,6 +73,16 @@ public class ResolvedSkill {
|
||||
@Builder.Default
|
||||
private boolean builtin = false;
|
||||
|
||||
/**
|
||||
* Skill row create timestamp, copied from {@code mate_skill.create_time}.
|
||||
* Used by the prompt-catalog ranker to surface freshly installed skills
|
||||
* before they accumulate any usage stats — without this, a brand-new
|
||||
* skill stays invisible behind the recent/frequent/alphabetical sort
|
||||
* and the LLM ends up replying "no such skill" right after the user
|
||||
* installed it. Null for virtual MCP/ACP skills that don't own a row.
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
// ==================== 安全扫描状态 ====================
|
||||
|
||||
/** 是否被安全扫描阻断 */
|
||||
|
||||
@ -265,7 +265,7 @@ public class SkillFileTool {
|
||||
}
|
||||
|
||||
@Tool(description = """
|
||||
List all currently available Skills (documentation packages).
|
||||
List currently available Skills (documentation packages).
|
||||
|
||||
IMPORTANT: Skills are NOT directly callable as tools. Each name
|
||||
returned here is a `skillName` argument, not a tool name. To use
|
||||
@ -273,6 +273,17 @@ public class SkillFileTool {
|
||||
first to read its instructions, then follow what SKILL.md tells you.
|
||||
Calling a skill name as a tool will fail with "Tool not found".
|
||||
|
||||
Search strategy when looking for a specific skill:
|
||||
- The default page is 20 of N — if "Showing: 20 of <larger>" appears
|
||||
and you don't see what you're after, retry with `keyword=<name fragment>`
|
||||
(matched against name + description, case-insensitive) or raise `limit`
|
||||
up to 50.
|
||||
- If the user mentions an exact skill name (e.g. "tencent-meeting-mcp"),
|
||||
skip this tool and go straight to
|
||||
`readSkillFile(skillName="<exact-name>", filePath="SKILL.md")` —
|
||||
that bypasses the catalog truncation entirely and either returns
|
||||
the skill's instructions or a clear "skill not found" error.
|
||||
|
||||
Note: this returns Skills (vendor-installable docs), not Agents.
|
||||
For Agents, use `listAvailableAgents`.
|
||||
|
||||
@ -280,7 +291,7 @@ public class SkillFileTool {
|
||||
""")
|
||||
public String listAvailableSkills(
|
||||
@JsonProperty(required = false)
|
||||
@JsonPropertyDescription("Optional keyword matched against skill name or description")
|
||||
@JsonPropertyDescription("Optional keyword matched against skill name or description (case-insensitive). Use this when a specific skill name was mentioned but didn't appear in the default page.")
|
||||
String keyword,
|
||||
|
||||
@JsonProperty(required = false)
|
||||
@ -299,6 +310,16 @@ public class SkillFileTool {
|
||||
|
||||
int safeLimit = limit == null || limit <= 0 ? 20 : Math.min(limit, 50);
|
||||
String kw = keyword == null ? "" : keyword.trim().toLowerCase();
|
||||
// Push freshly installed skills to the top of the truncated page so
|
||||
// a user who just installed something can still find it without
|
||||
// remembering to pass keyword=. Same window the prompt catalog uses.
|
||||
java.time.LocalDateTime recencyCutoff = java.time.LocalDateTime.now()
|
||||
.minus(SkillRuntimeService.NEW_SKILL_BOOST_WINDOW);
|
||||
// sortResolved gives the RECOMMENDED ordering; the secondary sort
|
||||
// below uses the JDK's stable sort to lift recently-installed skills
|
||||
// to the top while preserving RECOMMENDED order among same-recency
|
||||
// entries — no need to thread the (package-private) recommended
|
||||
// comparator back through here.
|
||||
List<ResolvedSkill> activeSkills = SkillCatalogSorter.sortResolved(
|
||||
runtimeService.getActiveSkills().stream()
|
||||
.filter(s -> SkillCatalogSorter.sourceMatches(s, source))
|
||||
@ -307,7 +328,10 @@ public class SkillFileTool {
|
||||
|| containsIgnoreCase(s.getName(), kw)
|
||||
|| containsIgnoreCase(s.getDescription(), kw))
|
||||
.toList(),
|
||||
SkillCatalogSort.RECOMMENDED);
|
||||
SkillCatalogSort.RECOMMENDED).stream()
|
||||
.sorted(java.util.Comparator.comparingInt((ResolvedSkill s) ->
|
||||
SkillRuntimeService.isRecentlyInstalled(s, recencyCutoff) ? 0 : 1))
|
||||
.toList();
|
||||
|
||||
if (activeSkills.isEmpty()) {
|
||||
return "No skills are currently available.";
|
||||
@ -338,8 +362,17 @@ public class SkillFileTool {
|
||||
}
|
||||
sb.append(" |\n");
|
||||
}
|
||||
sb.append("\nShowing: ").append(Math.min(safeLimit, activeSkills.size()))
|
||||
int shown = Math.min(safeLimit, activeSkills.size());
|
||||
sb.append("\nShowing: ").append(shown)
|
||||
.append(" of ").append(activeSkills.size()).append(" skill(s).");
|
||||
if (shown < activeSkills.size()) {
|
||||
// Surface the truncation hint so the LLM knows how to widen the
|
||||
// search instead of concluding the missing skill doesn't exist.
|
||||
sb.append(" Result truncated — retry with `keyword=<part of name>` ")
|
||||
.append("to search the full catalog, or `limit=50` to see more rows. ")
|
||||
.append("If the user gave an exact skill name, prefer ")
|
||||
.append("`readSkillFile(skillName=\"<name>\", filePath=\"SKILL.md\")` directly.");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user