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}