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
2 changes: 1 addition & 1 deletion agentscope-dependencies-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
<quartz.version>2.5.2</quartz.version>
<spring.version>7.0.3</spring.version>
<spring-boot.version>4.0.3</spring-boot.version>
<nacos-client.version>3.1.1</nacos-client.version>
<nacos-client.version>3.2.0-BETA</nacos-client.version>
<json-schema-validator.version>3.0.0</json-schema-validator.version>
<jsonschema-generator.version>4.38.0</jsonschema-generator.version>
<snakeyaml.version>2.6</snakeyaml.version>
Expand Down
7 changes: 7 additions & 0 deletions agentscope-distribution/agentscope-all/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-nacos-skill</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-skill-git-repository</artifactId>
Expand Down
6 changes: 6 additions & 0 deletions agentscope-distribution/agentscope-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@
<version>${project.version}</version>
</dependency>

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

<!-- AgentScope Nacos Spring Boot Starter -->
<dependency>
<groupId>io.agentscope</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2024-2026 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-nacos</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>agentscope-extensions-nacos-skill</artifactId>
<name>AgentScope Java - Nacos Skill Repository</name>
<description>agentscope-extensions-nacos-skill</description>

<dependencies>
<!-- Core library is needed at runtime for AgentSkillRepository, AgentSkill, etc. -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-core</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core.nacos.skill;

import com.alibaba.nacos.api.ai.AiService;
import com.alibaba.nacos.api.ai.model.skills.Skill;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.common.utils.StringUtils;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.repository.AgentSkillRepository;
import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Nacos-based implementation of {@link AgentSkillRepository}.
*
* <p>Reads skills from Nacos Config via {@code AiService.loadSkill(String)}. This implementation
* supports read operations: {@link #getSkill(String)}, {@link #skillExists(String)}, {@link
* #getRepositoryInfo()}, {@link #getSource()}, and {@link #isWriteable()}. List and write
* operations ({@link #getAllSkillNames()}, {@link #getAllSkills()}, {@link #save(List, boolean)},
* {@link #delete(String)})
* are implemented as read-only no-ops: they return empty list or {@code false} and log a warning.
*/
public class NacosSkillRepository implements AgentSkillRepository {

private static final Logger log = LoggerFactory.getLogger(NacosSkillRepository.class);

private static final String REPO_TYPE = "nacos";
private static final String LOCATION_PREFIX = "namespace:";

private final AiService aiService;
private final String namespaceId;
private final String source;
private final String location;

/**
* Creates a Nacos skill repository.
*
* @param aiService the Nacos AI service (must not be null)
* @param namespaceId the Nacos namespace ID (null or blank is treated as default namespace)
*/
public NacosSkillRepository(AiService aiService, String namespaceId) {
if (aiService == null) {
throw new IllegalArgumentException("AiService cannot be null");
}
this.aiService = aiService;
this.namespaceId = StringUtils.isBlank(namespaceId) ? "public" : namespaceId.trim();
this.source = REPO_TYPE + ":" + this.namespaceId;
this.location = LOCATION_PREFIX + this.namespaceId;
log.info("NacosSkillRepository initialized for namespace: {}", this.namespaceId);
}

@Override
public AgentSkill getSkill(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Skill name cannot be null or empty");
}
try {
Skill nacosSkill = aiService.loadSkill(name.trim());
if (nacosSkill == null) {
throw new IllegalArgumentException("Skill not found: " + name);
}
return NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, getSource());
} catch (NacosException e) {
if (e.getErrCode() == NacosException.NOT_FOUND) {
throw new IllegalArgumentException("Skill not found: " + name, e);
}
throw new RuntimeException("Failed to load skill from Nacos: " + name, e);
}
}

@Override
public boolean skillExists(String skillName) {
if (skillName == null || skillName.isBlank()) {
return false;
}
try {
Skill skill = aiService.loadSkill(skillName.trim());
return skill != null;
} catch (NacosException e) {
if (e.getErrCode() == NacosException.NOT_FOUND) {
return false;
}
log.warn("Error checking skill existence for {}: {}", skillName, e.getMessage());
return false;
}
}

@Override
public AgentSkillRepositoryInfo getRepositoryInfo() {
return new AgentSkillRepositoryInfo(REPO_TYPE, location, false);
}

@Override
public String getSource() {
return source;
}

@Override
public void setWriteable(boolean writeable) {
log.warn("NacosSkillRepository is read-only, set writeable operation ignored");
}

@Override
public boolean isWriteable() {
return false;
}

// ---------- Read-only no-op operations (list and write) ----------

@Override
public List<String> getAllSkillNames() {
log.warn("NacosSkillRepository is read-only, getAllSkillNames returns empty list");
return Collections.emptyList();
}

@Override
public List<AgentSkill> getAllSkills() {
log.warn("NacosSkillRepository is read-only, getAllSkills returns empty list");
return Collections.emptyList();
}

@Override
public boolean save(List<AgentSkill> skills, boolean force) {
log.warn("NacosSkillRepository is read-only, save operation ignored");
return false;
}

@Override
public boolean delete(String skillName) {
log.warn("NacosSkillRepository is read-only, delete operation ignored");
return false;
}

@Override
public void close() {
// AiService lifecycle is managed by the caller; nothing to release here
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.agentscope.core.nacos.skill;

import com.alibaba.nacos.api.ai.model.skills.Skill;
import com.alibaba.nacos.api.ai.model.skills.SkillResource;
import io.agentscope.core.skill.AgentSkill;
import java.util.HashMap;
import java.util.Map;

/**
* Converts Nacos AI {@link Skill} to AgentScope {@link AgentSkill}.
*/
public final class NacosSkillToAgentSkillConverter {

private static final String NO_DESCRIPTION = "(no description)";
private static final String NO_INSTRUCTION = "(no instruction)";

private NacosSkillToAgentSkillConverter() {}

/**
* Converts a Nacos Skill to an AgentSkill.
*
* @param nacosSkill the Nacos Skill (must not be null)
* @param source the source identifier for the resulting AgentSkill (e.g. "nacos:public")
* @return the converted AgentSkill
*/
public static AgentSkill toAgentSkill(Skill nacosSkill, String source) {
if (nacosSkill == null) {
throw new IllegalArgumentException("Nacos Skill cannot be null");
}
String name = blankToDefault(nacosSkill.getName(), "unknown");
String description = blankToDefault(nacosSkill.getDescription(), NO_DESCRIPTION);
String skillContent = blankToDefault(nacosSkill.getInstruction(), NO_INSTRUCTION);
Map<String, String> resources = toResourceMap(nacosSkill.getResource());
return new AgentSkill(name, description, skillContent, resources, source);
}

private static String blankToDefault(String value, String defaultValue) {
return (value != null && !value.isBlank()) ? value.trim() : defaultValue;
}

private static Map<String, String> toResourceMap(Map<String, SkillResource> resourceMap) {
if (resourceMap == null || resourceMap.isEmpty()) {
return new HashMap<>();
}
Map<String, String> result = new HashMap<>(resourceMap.size());
for (Map.Entry<String, SkillResource> e : resourceMap.entrySet()) {
String key = e.getKey() != null ? e.getKey() : "resource";
SkillResource res = e.getValue();
String content = (res != null && res.getContent() != null) ? res.getContent() : "";
result.put(key, content);
}
return result;
}
}
Loading
Loading