Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions data-agent-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
<version>${agentscope.version}</version>
</dependency>

<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-nacos-skill</artifactId>
<version>${agentscope.version}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,12 +49,15 @@ public class AgentService {
private final List<MarkAgentTool> 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) {
Expand Down Expand Up @@ -94,18 +99,10 @@ public Flux<ChatStreamEvent> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
* 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<AgentSkillRepository> repositories = new ArrayList<>();

public SkillBox createSkillBox(Toolkit toolkit) {
SkillBox skillBox = new SkillBox(toolkit);
List<AgentSkillRepository> repos = createRepositories();

for (AgentSkillRepository repo : repos) {
try {
List<AgentSkill> 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<AgentSkillRepository> createRepositories() {
List<AgentSkillRepository> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
* 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<FileSystemSource> filesystem = new ArrayList<>();
private List<GitSource> git = new ArrayList<>();
private List<ClasspathSource> classpath = new ArrayList<>();
private List<NacosSource> 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<String> skillNames = new ArrayList<>();
}
}
2 changes: 2 additions & 0 deletions data-agent-backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions data-agent-backend/src/main/resources/skill.properties
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions skills/data-query/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Loading