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