From 3e62ea0e544f243f97df7605db818536b8a30506 Mon Sep 17 00:00:00 2001 From: lzq986 <2719916844@qq.com> Date: Sat, 23 May 2026 21:40:43 +0800 Subject: [PATCH] feat(agent): add SkillLoaderService for multi-source skill loading - Add SkillLoaderService supporting FileSystem, Git, Nacos, and Classpath sources - Add SkillProperties for skill configuration management - Add agentscope-extensions-nacos-skill dependency for Nacos integration - Enable local-fs skill repository with ./skills directory - Integrate SkillBox into AgentService for skill-based tool invocation - Simplify agent system prompt to support dynamic skill loading --- data-agent-backend/pom.xml | 7 + .../github/malonetalk/agent/AgentService.java | 17 +- .../agent/skill/SkillLoaderService.java | 201 ++++++++++++++++++ .../agent/skill/SkillProperties.java | 72 +++++++ .../src/main/resources/application.properties | 2 + .../src/main/resources/skill.properties | 24 +++ skills/data-query/SKILL.md | 20 ++ 7 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillLoaderService.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillProperties.java create mode 100644 data-agent-backend/src/main/resources/skill.properties create mode 100644 skills/data-query/SKILL.md diff --git a/data-agent-backend/pom.xml b/data-agent-backend/pom.xml index 3760ef0..01bacbd 100644 --- a/data-agent-backend/pom.xml +++ b/data-agent-backend/pom.xml @@ -95,6 +95,13 @@ ${agentscope.version} + + io.agentscope + agentscope-extensions-nacos-skill + ${agentscope.version} + true + + org.projectlombok lombok diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java index bc285a2..7dabecb 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java @@ -23,9 +23,11 @@ import io.agentscope.core.memory.InMemoryMemory; import io.agentscope.core.message.Msg; import io.agentscope.core.session.Session; +import io.agentscope.core.skill.SkillBox; import io.agentscope.core.tool.Toolkit; import io.github.malonetalk.agent.models.ModelFactory; import io.github.malonetalk.agent.models.ModelProperties; +import io.github.malonetalk.agent.skill.SkillLoaderService; import io.github.malonetalk.agent.tools.MarkAgentTool; import io.github.malonetalk.convertor.EventConverter; import io.github.malonetalk.dto.ChatStreamEvent; @@ -47,12 +49,15 @@ public class AgentService { private final List allToolBeans; private final ModelProperties modelProperties; private final SessionService sessionService; + private final SkillLoaderService skillLoaderService; private Toolkit toolkit; + private SkillBox skillBox; @PostConstruct public void init() { this.toolkit = new Toolkit(); allToolBeans.forEach(this.toolkit::registerTool); + this.skillBox = skillLoaderService.createSkillBox(toolkit); } public String chat(String sessionId, String userInput) { @@ -94,18 +99,10 @@ public Flux chatStream(String sessionId, String userInput) { private ReActAgent createAgent() { return ReActAgent.builder() .name("DataAgent") - .sysPrompt( - """ - 你是一个数据助手,可以帮助用户查询数据库中的数据。请按以下步骤操作: - 1. 使用 get_tables 工具获取可用的数据库表信息 - 2. 根据用户问题,选择相关的表,使用 get_table_schema 工具获取表结构(列名、类型、主键等) - 3. 根据表结构信息,生成合适的 SELECT SQL 语句 - 4. 使用 execute_sql 工具执行 SQL 查询 - 5. 根据查询结果回答用户问题 - 注意:仅支持 SELECT 查询,不支持修改操作。生成SQL时请务必先查看表结构,确保列名和类型正确。 - """) + .sysPrompt("你是一个数据助手,可以帮助用户查询数据库中的数据。") .model(modelFactory.getInstance(modelProperties)) .toolkit(toolkit) + .skillBox(skillBox) .memory(new InMemoryMemory()) .maxIters(10) .build(); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillLoaderService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillLoaderService.java new file mode 100644 index 0000000..09d20cf --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillLoaderService.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.agent.skill; + +import com.alibaba.nacos.api.PropertyKeyConst; +import com.alibaba.nacos.api.ai.AiFactory; +import com.alibaba.nacos.api.ai.AiService; +import com.alibaba.nacos.api.exception.NacosException; +import io.agentscope.core.nacos.skill.NacosSkillRepository; +import io.agentscope.core.skill.AgentSkill; +import io.agentscope.core.skill.SkillBox; +import io.agentscope.core.skill.repository.AgentSkillRepository; +import io.agentscope.core.skill.repository.ClasspathSkillRepository; +import io.agentscope.core.skill.repository.FileSystemSkillRepository; +import io.agentscope.core.skill.repository.GitSkillRepository; +import io.agentscope.core.tool.Toolkit; +import jakarta.annotation.PreDestroy; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SkillLoaderService { + + private final SkillProperties skillProperties; + private final List repositories = new ArrayList<>(); + + public SkillBox createSkillBox(Toolkit toolkit) { + SkillBox skillBox = new SkillBox(toolkit); + List repos = createRepositories(); + + for (AgentSkillRepository repo : repos) { + try { + List skills = repo.getAllSkills(); + for (AgentSkill skill : skills) { + skillBox.registerSkill(skill); + log.info( + "Registered skill '{}' from source '{}'", + skill.getSkillId(), + skill.getSource()); + } + } catch (Exception e) { + log.error("Failed to load skills from repository: {}", repo.getRepositoryInfo(), e); + } + } + + // NacosSkillRepository.getAllSkills() returns an empty list because the Nacos AI + // API does not provide a list-all-skills endpoint. Therefore, skills must be + // loaded individually by name via getSkill(name), requiring the user to + // explicitly configure the skill-names list in application properties. + loadNacosSkillsByName(skillBox); + + return skillBox; + } + + // NacosSkillRepository.getAllSkills() always returns an empty list (the Nacos AI API + // lacks a list-all endpoint), so we load skills one by one using getSkill(name) + // based on the user-configured skill-names list. + private void loadNacosSkillsByName(SkillBox skillBox) { + for (SkillProperties.NacosSource ns : skillProperties.getNacos()) { + if (ns.getSkillNames().isEmpty()) { + continue; + } + try { + AiService aiService = createNacosAiService(ns); + Properties props = new Properties(); + if (ns.getSkillVersion() != null) { + props.setProperty( + NacosSkillRepository.SKILL_VERSION_PATH, ns.getSkillVersion()); + } + if (ns.getSkillLabel() != null) { + props.setProperty(NacosSkillRepository.SKILL_LABEL_PATH, ns.getSkillLabel()); + } + try (NacosSkillRepository repo = + new NacosSkillRepository(aiService, ns.getNamespace(), props)) { + for (String skillName : ns.getSkillNames()) { + try { + AgentSkill skill = repo.getSkill(skillName); + skillBox.registerSkill(skill); + log.info( + "Registered Nacos skill '{}' from namespace '{}'", + skill.getSkillId(), + ns.getNamespace()); + } catch (Exception e) { + log.error( + "Failed to load Nacos skill '{}' from namespace '{}'", + skillName, + ns.getNamespace(), + e); + } + } + } + } catch (Exception e) { + log.error( + "Failed to initialize NacosSkillRepository for namespace '{}'", + ns.getNamespace(), + e); + } + } + } + + List createRepositories() { + List repos = new ArrayList<>(); + + for (SkillProperties.FileSystemSource fs : skillProperties.getFilesystem()) { + try { + Path resolvedPath = Path.of(fs.getPath()).toAbsolutePath().normalize(); + log.info( + "FileSystemSkillRepository path: {} (resolved to: {})", + fs.getPath(), + resolvedPath); + FileSystemSkillRepository repo = + new FileSystemSkillRepository( + resolvedPath, fs.isWriteable(), fs.getSource()); + repos.add(repo); + } catch (Exception e) { + log.error("Failed to create FileSystemSkillRepository: {}", fs.getPath(), e); + } + } + + for (SkillProperties.GitSource gs : skillProperties.getGit()) { + try { + Path localPath = gs.getLocalPath() != null ? Path.of(gs.getLocalPath()) : null; + GitSkillRepository repo = + new GitSkillRepository( + gs.getUrl(), + gs.getBranch(), + localPath, + gs.getSource(), + gs.isAutoSync()); + repos.add(repo); + log.info("Created GitSkillRepository: {}", gs.getUrl()); + } catch (Exception e) { + log.error("Failed to create GitSkillRepository: {}", gs.getUrl(), e); + } + } + + for (SkillProperties.ClasspathSource cs : skillProperties.getClasspath()) { + try { + ClasspathSkillRepository repo = + new ClasspathSkillRepository(cs.getResourcePath(), cs.getSource()); + repos.add(repo); + log.info("Created ClasspathSkillRepository: {}", cs.getResourcePath()); + } catch (Exception e) { + log.error("Failed to create ClasspathSkillRepository: {}", cs.getResourcePath(), e); + } + } + + repositories.addAll(repos); + return repos; + } + + private AiService createNacosAiService(SkillProperties.NacosSource ns) throws NacosException { + Properties properties = new Properties(); + + properties.setProperty(PropertyKeyConst.SERVER_ADDR, ns.getServerAddr()); + if (ns.getUsername() != null) { + properties.setProperty(PropertyKeyConst.USERNAME, ns.getUsername()); + } + if (ns.getPassword() != null) { + properties.setProperty(PropertyKeyConst.PASSWORD, ns.getPassword()); + } + if (ns.getNamespace() != null) { + properties.setProperty(PropertyKeyConst.NAMESPACE, ns.getNamespace()); + } + return AiFactory.createAiService(properties); + } + + @PreDestroy + public void destroy() { + for (AgentSkillRepository repo : repositories) { + try { + repo.close(); + } catch (Exception e) { + log.warn("Failed to close repository: {}", repo.getRepositoryInfo(), e); + } + } + repositories.clear(); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillProperties.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillProperties.java new file mode 100644 index 0000000..63bcaab --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/skill/SkillProperties.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.agent.skill; + +import io.github.malonetalk.common.Constants; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = Constants.PROPERTIES_PREFIX + ".skill") +public class SkillProperties { + + private List filesystem = new ArrayList<>(); + private List git = new ArrayList<>(); + private List classpath = new ArrayList<>(); + private List nacos = new ArrayList<>(); + + @Data + public static class FileSystemSource { + private String path; + private boolean writeable = true; + private String source; + } + + @Data + public static class GitSource { + private String url; + private String branch; + private String localPath; + private String source; + private boolean autoSync = true; + } + + @Data + public static class ClasspathSource { + private String resourcePath; + private String source; + } + + @Data + public static class NacosSource { + private String serverAddr; + private String namespace; + private String username; + private String password; + private String skillVersion; + private String skillLabel; + private String source; + // NacosSkillRepository.getAllSkills() returns empty because the Nacos AI API has no + // list-all endpoint. Users must explicitly specify which skills to load by name. + private List skillNames = new ArrayList<>(); + } +} diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index 2858e1d..50ae6d6 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -25,3 +25,5 @@ io.github.malonetalk.model.provider=dashscope io.github.malonetalk.model.name=qwen3-max io.github.malonetalk.model.base-url= io.github.malonetalk.model.api-key=${DASHSCOPE_API_KEY:} + +spring.config.import=classpath:skill.properties diff --git a/data-agent-backend/src/main/resources/skill.properties b/data-agent-backend/src/main/resources/skill.properties new file mode 100644 index 0000000..c4d6673 --- /dev/null +++ b/data-agent-backend/src/main/resources/skill.properties @@ -0,0 +1,24 @@ +# Skill Configuration - FileSystem +io.github.malonetalk.skill.filesystem[0].path=./skills +io.github.malonetalk.skill.filesystem[0].writeable=true +io.github.malonetalk.skill.filesystem[0].source=local-fs + +# Skill Configuration - Git +#io.github.malonetalk.skill.git[0].url=https://github.com/your-org/your-skills-repo.git +#io.github.malonetalk.skill.git[0].branch=main +#io.github.malonetalk.skill.git[0].auto-sync=true +#io.github.malonetalk.skill.git[0].source=git-repo + +# Skill Configuration - Classpath +#io.github.malonetalk.skill.classpath[0].resource-path=skills +#io.github.malonetalk.skill.classpath[0].source=classpath-skills + +# Skill Configuration - Nacos +#io.github.malonetalk.skill.nacos[0].server-addr=localhost:8848 +#io.github.malonetalk.skill.nacos[0].namespace=public +#io.github.malonetalk.skill.nacos[0].username=nacos +#io.github.malonetalk.skill.nacos[0].password=nacos +#io.github.malonetalk.skill.nacos[0].source=nacos-skills +# NacosSkillRepository.getAllSkills() returns empty (no list-all API), so skill-names must be specified explicitly +#io.github.malonetalk.skill.nacos[0].skill-names[0]=data-analysis +#io.github.malonetalk.skill.nacos[0].skill-names[1]=report-generation diff --git a/skills/data-query/SKILL.md b/skills/data-query/SKILL.md new file mode 100644 index 0000000..afcb7f8 --- /dev/null +++ b/skills/data-query/SKILL.md @@ -0,0 +1,20 @@ +--- +name: data-query +description: A skill for querying database tables and generating SQL queries based on table schemas. +--- + +# Data Query Skill + +You are a data query assistant. When the user asks a data-related question, follow these steps: + +1. Use the `get_tables` tool to get available database tables +2. Use the `get_table_schema` tool to get the schema of relevant tables +3. Generate a SELECT SQL statement based on the table structure +4. Use the `execute_sql` tool to execute the query +5. Summarize the query results for the user + +## Notes + +- Only SELECT queries are supported, no modification operations +- Always check the table schema before generating SQL to ensure column names and types are correct +- If the query result is empty, suggest the user check the query conditions