001/* 002 * Copyright 2025 - 2025 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.skill; 017 018 019import com.agentsflex.core.model.chat.tool.Parameter; 020import com.agentsflex.core.model.chat.tool.Tool; 021import com.agentsflex.skill.util.Skills; 022 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.List; 026import java.util.stream.Collectors; 027 028/** 029 * @author Christian Tzolov 030 * @author Micahel Yang 031 */ 032public class SkillsTool { 033 034 private static final String TOOL_DESCRIPTION_TEMPLATE = "Execute a skill within the main conversation\n" + 035 "\n" + 036 "<skills_instructions>\n" + 037 "When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n" + 038 "\n" + 039 "How to use skills:\n" + 040 "- Invoke skills using this tool with the skill name only (no arguments)\n" + 041 "- When you invoke a skill, you will see <command-message>The \"{name}\" skill is loading</command-message>\n" + 042 "- The skill's prompt will expand and provide detailed instructions on how to complete the task\n" + 043 "\n" + 044 "NOTE: Response always starts start with the base directory of the skill execution environment. You can use this to retrieve additional files of call shell commands.\n" + 045 "Skill description follows after the base directory line.\n" + 046 "\n" + 047 "Important:\n" + 048 "- Only use skills listed in <available_skills> below\n" + 049 "- Do not invoke a skill that is already running\n" + 050 "</skills_instructions>\n" + 051 "\n" + 052 "<available_skills>\n" + 053 "%s\n" + 054 "</available_skills>"; 055 056 057 public static Builder builder() { 058 return new Builder(); 059 } 060 061 public static class Builder { 062 063 private List<Skill> skills = new ArrayList<>(); 064 065 private String toolDescriptionTemplate = TOOL_DESCRIPTION_TEMPLATE; 066 067 protected Builder() { 068 069 } 070 071 public Builder toolDescriptionTemplate(String template) { 072 this.toolDescriptionTemplate = template; 073 return this; 074 } 075 076 077 public Builder addSkillsDirectory(String skillsRootDirectory) { 078 this.addSkillsDirectories(Collections.singletonList(skillsRootDirectory)); 079 return this; 080 } 081 082 public Builder addSkillsDirectories(List<String> skillsRootDirectories) { 083 for (String skillsRootDirectory : skillsRootDirectories) { 084 this.skills.addAll(Skills.loadDirectory(skillsRootDirectory)); 085 } 086 return this; 087 } 088 089 public Tool build() { 090 String skillsXml = this.skills.stream().map(s -> s.toXml()).collect(Collectors.joining("\n")); 091 return Tool.builder() 092 .name("Skill") 093 .description(String.format(this.toolDescriptionTemplate, skillsXml)) 094 .addParameter( 095 Parameter.builder() 096 .name("command") 097 .type("string") 098 .description("The skill name (no arguments). E.g., \"pdf\" or \"xlsx\"").build() 099 ) 100 .function(stringStringMap -> { 101 String command = (String) stringStringMap.get("command"); 102 Skill skill = null; 103 for (Skill s : skills) { 104 if (s.name().equals(command)) { 105 skill = s; 106 break; 107 } 108 } 109 110 if (skill != null) { 111 return String.format("Base directory for this skill: %s\n\n%s", skill.getBasePath(), skill.getContent()); 112 } 113 114 return "Skill not found: " + command; 115 }).build(); 116 } 117 } 118}