Skip to content

Commit fc65583

Browse files
authored
feat(nacos): For #293, add Nacos integration for AgentScope A2A registry and discovery. (#387)
1 parent d40aea3 commit fc65583

20 files changed

Lines changed: 3509 additions & 2 deletions

File tree

agentscope-dependencies-bom/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
<xxl-job.version>3.3.1</xxl-job.version>
9898
<spring.version>7.0.2</spring.version>
9999
<spring-boot.version>4.0.1</spring-boot.version>
100+
<nacos-client.version>3.1.1</nacos-client.version>
100101

101102
<spotless.version>3.1.0</spotless.version>
102103
<maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>
@@ -331,6 +332,13 @@
331332
<artifactId>spring-boot-autoconfigure</artifactId>
332333
<version>${spring-boot.version}</version>
333334
</dependency>
335+
336+
<!-- Nacos dependencies -->
337+
<dependency>
338+
<groupId>com.alibaba.nacos</groupId>
339+
<artifactId>nacos-client</artifactId>
340+
<version>${nacos-client.version}</version>
341+
</dependency>
334342
</dependencies>
335343
</dependencyManagement>
336344

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2024-2025 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ You may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>io.agentscope</groupId>
24+
<artifactId>agentscope-extensions-nacos</artifactId>
25+
<version>${revision}</version>
26+
<relativePath>../pom.xml</relativePath>
27+
</parent>
28+
29+
<artifactId>agentscope-extensions-nacos-a2a</artifactId>
30+
<name>AgentScope Java - Nacos A2A Registry</name>
31+
<description>AgentScope Java - integration with Nacos as A2A registry</description>
32+
33+
<dependencies>
34+
<dependency>
35+
<groupId>com.alibaba.nacos</groupId>
36+
<artifactId>nacos-client</artifactId>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>io.agentscope</groupId>
41+
<artifactId>agentscope-extensions-a2a-client</artifactId>
42+
<scope>provided</scope>
43+
<optional>true</optional>
44+
</dependency>
45+
46+
<dependency>
47+
<groupId>io.agentscope</groupId>
48+
<artifactId>agentscope-extensions-a2a-server</artifactId>
49+
<scope>provided</scope>
50+
<optional>true</optional>
51+
</dependency>
52+
</dependencies>
53+
54+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.agentscope.core.nacos.a2a.discovery;
18+
19+
import com.alibaba.nacos.api.ai.AiFactory;
20+
import com.alibaba.nacos.api.ai.AiService;
21+
import com.alibaba.nacos.api.ai.listener.AbstractNacosAgentCardListener;
22+
import com.alibaba.nacos.api.ai.listener.NacosAgentCardEvent;
23+
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
24+
import com.alibaba.nacos.api.exception.NacosException;
25+
import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException;
26+
import io.a2a.spec.AgentCard;
27+
import io.agentscope.core.a2a.agent.A2aAgent;
28+
import io.agentscope.core.a2a.agent.card.AgentCardResolver;
29+
import io.agentscope.core.nacos.a2a.utils.AgentCardConverterUtil;
30+
import java.util.Map;
31+
import java.util.Properties;
32+
import java.util.concurrent.ConcurrentHashMap;
33+
34+
/**
35+
* Agent Card Producer from Nacos A2A Registry.
36+
*
37+
* <p>Example Usage:
38+
* <pre>{@code
39+
* Properties properties = new Properties();
40+
* properties.put(PropertyKeyConst.SERVER_ADDR, "localhost:8848");
41+
* // put other Nacos server properties
42+
* // create NacosAgentCardResolver with new Nacos Client
43+
* NacosAgentCardResolver nacosAgentCardResolver = new NacosAgentCardResolver(properties);
44+
* // Equals like:
45+
* // AiService a2aService = AiFactory.createAiService(properties);
46+
* // NacosAgentCardResolver nacosAgentCardResolver = new NacosAgentCardResolver(a2aService)
47+
*
48+
* // Or Reuse Nacos Client
49+
* AiService a2aService = getAiServiceFromCacheOrOtherComponent();
50+
* NacosAgentCardResolver nacosAgentCardResolver = new NacosAgentCardResolver(a2aService);
51+
*
52+
* // Then new A2aAgent
53+
* A2aAgent a2aAgent = A2aAgent.builder().name("remote-agent-name").agentCardResolver(nacosAgentCardResolver).build();
54+
* }</pre>
55+
*
56+
* @see A2aAgent
57+
* @see A2aAgent.Builder
58+
*/
59+
public class NacosAgentCardResolver implements AgentCardResolver {
60+
61+
private static final int DEFAULT_INITIAL_CAPACITY = 2;
62+
63+
private final AiService aiService;
64+
65+
private final Map<String, AgentCard> agentCardCaches;
66+
67+
private final Map<String, AgentCardUpdater> agentCardUpdaters;
68+
69+
/**
70+
* Creates a new instance for {@link NacosAgentCardResolver}.
71+
*
72+
* @param properties properties for nacos server to create nacos client AI service
73+
* @throws NacosException during building nacos client
74+
*/
75+
public NacosAgentCardResolver(Properties properties) throws NacosException {
76+
this(AiFactory.createAiService(properties));
77+
}
78+
79+
/**
80+
* Creates a new instance for {@link NacosAgentCardResolver}.
81+
*
82+
* @param aiService nacos client AI service
83+
*/
84+
public NacosAgentCardResolver(AiService aiService) {
85+
this.aiService = aiService;
86+
this.agentCardCaches = new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY);
87+
this.agentCardUpdaters = new ConcurrentHashMap<>(DEFAULT_INITIAL_CAPACITY);
88+
}
89+
90+
@Override
91+
public AgentCard getAgentCard(String agentName) {
92+
AgentCard cachedAgentCard = agentCardCaches.get(agentName);
93+
if (cachedAgentCard != null) {
94+
return cachedAgentCard;
95+
}
96+
AgentCard result = getAndSubscribe(agentName);
97+
// If already put by listener, use listener put value
98+
return agentCardCaches.computeIfAbsent(agentName, name -> result);
99+
}
100+
101+
private AgentCard getAndSubscribe(String agentName) {
102+
try {
103+
AgentCardUpdater updater =
104+
agentCardUpdaters.computeIfAbsent(agentName, name -> new AgentCardUpdater());
105+
AgentCardDetailInfo agentCardDetailInfo =
106+
aiService.subscribeAgentCard(agentName, updater);
107+
return AgentCardConverterUtil.convertToA2aAgentCard(agentCardDetailInfo);
108+
} catch (NacosException e) {
109+
throw new NacosRuntimeException(e.getErrCode(), e.getErrMsg(), e);
110+
}
111+
}
112+
113+
private class AgentCardUpdater extends AbstractNacosAgentCardListener {
114+
115+
@Override
116+
public void onEvent(NacosAgentCardEvent event) {
117+
agentCardCaches.put(
118+
event.getAgentName(),
119+
AgentCardConverterUtil.convertToA2aAgentCard(event.getAgentCard()));
120+
}
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.agentscope.core.nacos.a2a.registry;
18+
19+
import com.alibaba.nacos.api.ai.A2aService;
20+
import com.alibaba.nacos.api.ai.AiFactory;
21+
import com.alibaba.nacos.api.ai.constant.AiConstants;
22+
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
23+
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
24+
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
25+
import com.alibaba.nacos.api.exception.NacosException;
26+
import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException;
27+
import io.agentscope.core.nacos.a2a.utils.AgentCardConverterUtil;
28+
import java.util.Properties;
29+
import java.util.Set;
30+
import java.util.stream.Collectors;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
/**
35+
* AgentScope Extensions for Registry A2A AgentCard and A2A instance endpoint to Nacos.
36+
*/
37+
public class NacosA2aRegistry {
38+
39+
private static final Logger log = LoggerFactory.getLogger(NacosA2aRegistry.class);
40+
41+
private final A2aService a2aService;
42+
43+
/**
44+
* New instance of {@link NacosA2aRegistry} by Nacos server properties.
45+
*
46+
* @param nacosProperties the properties for Nacos server
47+
* @throws NacosException during building Nacos client failed
48+
*/
49+
public NacosA2aRegistry(Properties nacosProperties) throws NacosException {
50+
this(AiFactory.createAiService(nacosProperties));
51+
}
52+
53+
/**
54+
* New instance of {@link NacosA2aRegistry} by Nacos Client instance.
55+
*
56+
* @param a2aService the Nacos A2aService instance, which also can use {@link com.alibaba.nacos.api.ai.AiService}.
57+
*/
58+
public NacosA2aRegistry(A2aService a2aService) {
59+
this.a2aService = a2aService;
60+
}
61+
62+
/**
63+
* Registers an A2A agent card and endpoint to Nacos.
64+
*
65+
* @param agentCard the agent card to register
66+
* @param a2aProperties the properties for A2A registry
67+
*/
68+
public void registerAgent(
69+
io.a2a.spec.AgentCard agentCard, NacosA2aRegistryProperties a2aProperties) {
70+
AgentCard nacosAgentCard = AgentCardConverterUtil.convertToNacosAgentCard(agentCard);
71+
try {
72+
tryReleaseAgentCard(nacosAgentCard, a2aProperties);
73+
registerEndpoint(nacosAgentCard, a2aProperties);
74+
} catch (NacosException e) {
75+
log.error("Register agent card {} to Nacos failed.", agentCard.name(), e);
76+
throw new NacosRuntimeException(e.getErrCode(), e.getErrMsg(), e);
77+
}
78+
}
79+
80+
private void tryReleaseAgentCard(AgentCard agentCard, NacosA2aRegistryProperties a2aProperties)
81+
throws NacosException {
82+
if (null != tryGetAgentCardFromNacos(agentCard)) {
83+
log.warn(
84+
"Agent card {} already exists, agentCard release might be ignored.",
85+
agentCard.getName());
86+
}
87+
log.info("Register agent card {} to Nacos.", agentCard.getName());
88+
a2aService.releaseAgentCard(
89+
agentCard,
90+
AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE,
91+
a2aProperties.isSetAsLatest());
92+
log.info("Register agent card {} to Nacos successfully.", agentCard.getName());
93+
}
94+
95+
private AgentCardDetailInfo tryGetAgentCardFromNacos(AgentCard agentCard)
96+
throws NacosException {
97+
try {
98+
return a2aService.getAgentCard(agentCard.getName(), agentCard.getVersion());
99+
} catch (NacosException ignored) {
100+
return null;
101+
}
102+
}
103+
104+
private void registerEndpoint(AgentCard agentCard, NacosA2aRegistryProperties a2aProperties)
105+
throws NacosException {
106+
if (!a2aProperties.enabledRegisterEndpoint()) {
107+
log.info("Disabled register endpoint(s) to Agent, skip endpoint(s) register step.");
108+
return;
109+
}
110+
if (a2aProperties.transportProperties().isEmpty()) {
111+
log.warn("No endpoint(s) found, skip endpoint(s) register step.");
112+
return;
113+
}
114+
log.info("Register {} endpoint(s) to Nacos", a2aProperties.transportProperties().size());
115+
if (a2aProperties.transportProperties().size() == 1) {
116+
AgentEndpoint endpoint =
117+
buildAgentEndpoint(
118+
a2aProperties.transportProperties().values().iterator().next(),
119+
agentCard.getVersion());
120+
a2aService.registerAgentEndpoint(agentCard.getName(), endpoint);
121+
} else {
122+
Set<AgentEndpoint> endpoints =
123+
a2aProperties.transportProperties().values().stream()
124+
.map(
125+
transportProperties ->
126+
buildAgentEndpoint(
127+
transportProperties, agentCard.getVersion()))
128+
.collect(Collectors.toSet());
129+
a2aService.registerAgentEndpoint(agentCard.getName(), endpoints);
130+
}
131+
}
132+
133+
private AgentEndpoint buildAgentEndpoint(
134+
NacosA2aRegistryTransportProperties transportProperties, String version) {
135+
AgentEndpoint result = new AgentEndpoint();
136+
result.setTransport(transportProperties.transport());
137+
result.setAddress(transportProperties.host());
138+
result.setPort(transportProperties.port());
139+
result.setPath(transportProperties.path());
140+
result.setSupportTls(transportProperties.supportTls());
141+
result.setVersion(version);
142+
result.setProtocol(transportProperties.protocol());
143+
result.setQuery(transportProperties.query());
144+
return result;
145+
}
146+
}

0 commit comments

Comments
 (0)