mirror of https://github.com/alibaba/nacos.git
[ISSUE#13322] Refactor a2a registry implementation (#13794)
* Support a2a client api. * Support query agent card with service type agent card. * Refactor agent name encode to id by AgentIdCodec. * Rename admin/console api parameter name -> agentName. * Support agent name check. * Fix pmd * Fix unit test. * Fix unit test.
This commit is contained in:
parent
2163a6b337
commit
9df5bcc1ed
|
@ -96,5 +96,13 @@ public class Constants {
|
|||
public static final String SEARCH_BLUR = "blur";
|
||||
|
||||
public static final String SEARCH_ACCURATE = "accurate";
|
||||
|
||||
public static final String AGENT_ENDPOINT_GROUP = "agent-endpoints";
|
||||
|
||||
public static final String AGENT_ENDPOINT_PATH_KEY = "__nacos.agent.endpoint.path__";
|
||||
|
||||
public static final String AGENT_ENDPOINT_TRANSPORT_KEY = "__nacos.agent.endpoint.transport__";
|
||||
|
||||
public static final String NACOS_AGENT_ENDPOINT_SUPPORT_TLS = "__nacos.agent.endpoint.supportTls__";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,8 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
|
|||
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
|
||||
import com.alibaba.nacos.ai.service.A2aServerOperationService;
|
||||
import com.alibaba.nacos.ai.param.AgentHttpParamExtractor;
|
||||
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
|
@ -34,6 +35,7 @@ import com.alibaba.nacos.api.model.Page;
|
|||
import com.alibaba.nacos.api.model.v2.Result;
|
||||
import com.alibaba.nacos.auth.annotation.Secured;
|
||||
import com.alibaba.nacos.core.model.form.PageForm;
|
||||
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ApiType;
|
||||
import com.alibaba.nacos.plugin.auth.constant.SignType;
|
||||
|
@ -54,6 +56,7 @@ import java.util.List;
|
|||
@NacosApi
|
||||
@RestController
|
||||
@RequestMapping(Constants.A2A.ADMIN_PATH)
|
||||
@ExtractorManager.Extractor(httpExtractor = AgentHttpParamExtractor.class)
|
||||
public class A2aAdminController {
|
||||
|
||||
private final A2aServerOperationService a2aServerOperationService;
|
||||
|
@ -90,7 +93,8 @@ public class A2aAdminController {
|
|||
public Result<AgentCardDetailInfo> getAgentCard(AgentForm form) throws NacosApiException {
|
||||
form.validate();
|
||||
return Result.success(
|
||||
a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getName(), form.getVersion()));
|
||||
a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getAgentName(), form.getVersion(),
|
||||
form.getRegistrationType()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,7 +125,7 @@ public class A2aAdminController {
|
|||
@Secured(action = ActionTypes.WRITE, signType = SignType.AI, apiType = ApiType.ADMIN_API)
|
||||
public Result<String> deleteAgent(AgentForm form) throws NacosException {
|
||||
form.validate();
|
||||
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getName(), form.getVersion());
|
||||
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getAgentName(), form.getVersion());
|
||||
return Result.success("ok");
|
||||
}
|
||||
|
||||
|
@ -140,7 +144,7 @@ public class A2aAdminController {
|
|||
agentListForm.validate();
|
||||
pageForm.validate();
|
||||
return Result.success(
|
||||
a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getName(),
|
||||
a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
|
||||
agentListForm.getSearch(), pageForm.getPageNo(), pageForm.getPageSize()));
|
||||
}
|
||||
|
||||
|
@ -156,6 +160,6 @@ public class A2aAdminController {
|
|||
public Result<List<AgentVersionDetail>> listAgentVersions(AgentForm agentForm) throws NacosException {
|
||||
agentForm.validate();
|
||||
return Result.success(
|
||||
a2aServerOperationService.listAgentVersions(agentForm.getNamespaceId(), agentForm.getName()));
|
||||
a2aServerOperationService.listAgentVersions(agentForm.getNamespaceId(), agentForm.getAgentName()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ public class AgentDetailForm extends AgentForm {
|
|||
public void validate() throws NacosApiException {
|
||||
fillDefaultNamespaceId();
|
||||
|
||||
if (StringUtils.isEmpty(super.getName())) {
|
||||
if (StringUtils.isEmpty(super.getAgentName())) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter 'name' type String is not present");
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ public class AgentForm implements NacosForm {
|
|||
|
||||
private String namespaceId;
|
||||
|
||||
private String name;
|
||||
private String agentName;
|
||||
|
||||
private String version;
|
||||
|
||||
|
@ -49,7 +49,7 @@ public class AgentForm implements NacosForm {
|
|||
@Override
|
||||
public void validate() throws NacosApiException {
|
||||
fillDefaultNamespaceId();
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
if (StringUtils.isEmpty(agentName)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter 'name' type String is not present");
|
||||
}
|
||||
|
@ -69,12 +69,12 @@ public class AgentForm implements NacosForm {
|
|||
this.namespaceId = namespaceId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
public String getAgentName() {
|
||||
return agentName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
public void setAgentName(String agentName) {
|
||||
this.agentName = agentName;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
|
@ -99,13 +99,13 @@ public class AgentForm implements NacosForm {
|
|||
return false;
|
||||
}
|
||||
AgentForm agentForm = (AgentForm) o;
|
||||
return Objects.equals(namespaceId, agentForm.namespaceId) && Objects.equals(name, agentForm.name)
|
||||
return Objects.equals(namespaceId, agentForm.namespaceId) && Objects.equals(agentName, agentForm.agentName)
|
||||
&& Objects.equals(version, agentForm.version) && Objects.equals(registrationType,
|
||||
agentForm.registrationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(namespaceId, name, version, registrationType);
|
||||
return Objects.hash(namespaceId, agentName, version, registrationType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.param;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.common.paramcheck.ParamInfo;
|
||||
import com.alibaba.nacos.common.utils.JacksonUtils;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.core.paramcheck.AbstractHttpParamExtractor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Nacos AI Agent or AgentCard param extractor.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AgentHttpParamExtractor extends AbstractHttpParamExtractor {
|
||||
|
||||
private static final String AGENT_CARD_PARAM = "agentCard";
|
||||
|
||||
@Override
|
||||
public List<ParamInfo> extractParam(HttpServletRequest request) throws NacosException {
|
||||
ParamInfo paramInfo = new ParamInfo();
|
||||
paramInfo.setNamespaceId(request.getParameter("namespaceId"));
|
||||
paramInfo.setAgentName(request.getParameter("agentName"));
|
||||
if (request.getParameterMap().containsKey(AGENT_CARD_PARAM)) {
|
||||
paramInfo.setAgentName(deserializeAndGetAgentName(request.getParameter(AGENT_CARD_PARAM)));
|
||||
}
|
||||
return Collections.singletonList(paramInfo);
|
||||
}
|
||||
|
||||
private String deserializeAndGetAgentName(String agentCardJson) {
|
||||
try {
|
||||
AgentCard agentCard = JacksonUtils.toObj(agentCardJson, AgentCard.class);
|
||||
return agentCard.getName();
|
||||
} catch (Exception ignored) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import com.alibaba.nacos.ai.index.McpServerIndex;
|
|||
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
|
||||
import com.alibaba.nacos.ai.service.McpServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtil;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtils;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.FrontEndpointConfig;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
|
||||
|
@ -78,7 +77,7 @@ public class McpServerEndpointRequestHandler
|
|||
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
|
||||
public McpServerEndpointResponse handle(McpServerEndpointRequest request, RequestMeta meta) throws NacosException {
|
||||
McpRequestUtils.fillNamespaceId(request);
|
||||
McpRequestUtil.fillNamespaceId(request);
|
||||
try {
|
||||
checkParameters(request);
|
||||
Instance instance = buildInstance(request);
|
||||
|
|
|
@ -19,7 +19,7 @@ package com.alibaba.nacos.ai.remote.handler;
|
|||
import com.alibaba.nacos.ai.index.McpServerIndex;
|
||||
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
|
||||
import com.alibaba.nacos.ai.service.McpServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtils;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.remote.request.QueryMcpServerRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse;
|
||||
|
@ -58,7 +58,7 @@ public class QueryMcpServerRequestHandler extends RequestHandler<QueryMcpServerR
|
|||
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.READ, signType = SignType.AI)
|
||||
public QueryMcpServerResponse handle(QueryMcpServerRequest request, RequestMeta meta) throws NacosException {
|
||||
McpRequestUtils.fillNamespaceId(request);
|
||||
McpRequestUtil.fillNamespaceId(request);
|
||||
if (StringUtils.isBlank(request.getMcpName())) {
|
||||
QueryMcpServerResponse errorResponse = new QueryMcpServerResponse();
|
||||
errorResponse.setErrorInfo(NacosException.INVALID_PARAM, "parameters `mcpName` can't be empty or null");
|
||||
|
|
|
@ -20,7 +20,7 @@ import com.alibaba.nacos.ai.index.McpServerIndex;
|
|||
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
|
||||
import com.alibaba.nacos.ai.service.McpEndpointOperationService;
|
||||
import com.alibaba.nacos.ai.service.McpServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtils;
|
||||
import com.alibaba.nacos.ai.utils.McpRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
|
||||
|
@ -73,7 +73,7 @@ public class ReleaseMcpServerRequestHandler extends RequestHandler<ReleaseMcpSer
|
|||
@ExtractorManager.Extractor(rpcExtractor = McpServerRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
|
||||
public ReleaseMcpServerResponse handle(ReleaseMcpServerRequest request, RequestMeta meta) throws NacosException {
|
||||
McpRequestUtils.fillNamespaceId(request);
|
||||
McpRequestUtil.fillNamespaceId(request);
|
||||
try {
|
||||
checkParameters(request);
|
||||
return doHandler(request, meta);
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.remote.handler.a2a;
|
||||
|
||||
import com.alibaba.nacos.ai.constant.Constants;
|
||||
import com.alibaba.nacos.ai.service.a2a.identity.AgentIdCodecHolder;
|
||||
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.exception.api.NacosApiException;
|
||||
import com.alibaba.nacos.api.model.v2.ErrorCode;
|
||||
import com.alibaba.nacos.api.naming.pojo.Instance;
|
||||
import com.alibaba.nacos.api.remote.request.RequestMeta;
|
||||
import com.alibaba.nacos.auth.annotation.Secured;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
|
||||
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
|
||||
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
|
||||
import com.alibaba.nacos.core.remote.RequestHandler;
|
||||
import com.alibaba.nacos.naming.core.v2.pojo.Service;
|
||||
import com.alibaba.nacos.naming.core.v2.service.impl.EphemeralClientOperationServiceImpl;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
|
||||
import com.alibaba.nacos.plugin.auth.constant.SignType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Register or Deregister endpoint for agent to nacos AI module request handler.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
@Component
|
||||
public class AgentEndpointRequestHandler extends RequestHandler<AgentEndpointRequest, AgentEndpointResponse> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AgentEndpointRequestHandler.class);
|
||||
|
||||
private final EphemeralClientOperationServiceImpl clientOperationService;
|
||||
|
||||
private final AgentIdCodecHolder agentIdCodecHolder;
|
||||
|
||||
public AgentEndpointRequestHandler(EphemeralClientOperationServiceImpl clientOperationService,
|
||||
AgentIdCodecHolder agentIdCodecHolder) {
|
||||
this.clientOperationService = clientOperationService;
|
||||
this.agentIdCodecHolder = agentIdCodecHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NamespaceValidation
|
||||
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
|
||||
public AgentEndpointResponse handle(AgentEndpointRequest request, RequestMeta meta) throws NacosException {
|
||||
AgentEndpointResponse response = new AgentEndpointResponse();
|
||||
AgentRequestUtil.fillNamespaceId(request);
|
||||
try {
|
||||
validateRequest(request);
|
||||
Instance instance = transferInstance(request);
|
||||
String serviceName =
|
||||
agentIdCodecHolder.encode(request.getAgentName()) + "::" + request.getEndpoint().getVersion();
|
||||
Service service = Service.newService(request.getNamespaceId(), Constants.A2A.AGENT_ENDPOINT_GROUP,
|
||||
serviceName);
|
||||
switch (request.getType()) {
|
||||
case AiRemoteConstants.REGISTER_ENDPOINT:
|
||||
doRegisterEndpoint(service, instance, meta);
|
||||
break;
|
||||
case AiRemoteConstants.DE_REGISTER_ENDPOINT:
|
||||
doDeregisterEndpoint(service, instance, meta);
|
||||
break;
|
||||
default:
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_VALIDATE_ERROR,
|
||||
String.format("parameter `type` should be %s or %s, but was %s",
|
||||
AiRemoteConstants.REGISTER_ENDPOINT, AiRemoteConstants.DE_REGISTER_ENDPOINT,
|
||||
request.getType()));
|
||||
}
|
||||
} catch (NacosApiException e) {
|
||||
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
|
||||
LOGGER.error("[{}] Register agent endpoint to agent {} error: {}", meta.getConnectionId(),
|
||||
request.getAgentName(), e.getErrMsg());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private Instance transferInstance(AgentEndpointRequest request) throws NacosApiException {
|
||||
Instance instance = new Instance();
|
||||
AgentEndpoint endpoint = request.getEndpoint();
|
||||
instance.setIp(endpoint.getAddress());
|
||||
instance.setPort(endpoint.getPort());
|
||||
String path = StringUtils.isBlank(endpoint.getPath()) ? StringUtils.EMPTY : endpoint.getPath();
|
||||
Map<String, String> metadata = Map.of(Constants.A2A.AGENT_ENDPOINT_PATH_KEY, path,
|
||||
Constants.A2A.AGENT_ENDPOINT_TRANSPORT_KEY, endpoint.getTransport(),
|
||||
Constants.A2A.NACOS_AGENT_ENDPOINT_SUPPORT_TLS, String.valueOf(endpoint.isSupportTls()));
|
||||
instance.setMetadata(metadata);
|
||||
instance.validate();
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void validateRequest(AgentEndpointRequest request) throws NacosApiException {
|
||||
if (StringUtils.isBlank(request.getAgentName())) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter `agentName` can't be empty or null");
|
||||
}
|
||||
if (null == request.getEndpoint()) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter `endpoint` can't be null");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getEndpoint().getVersion())) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter `endpoint.version` can't be empty or null");
|
||||
}
|
||||
}
|
||||
|
||||
private void doRegisterEndpoint(Service service, Instance instance, RequestMeta meta) throws NacosException {
|
||||
clientOperationService.registerInstance(service, instance, meta.getConnectionId());
|
||||
|
||||
}
|
||||
|
||||
private void doDeregisterEndpoint(Service service, Instance instance, RequestMeta meta) {
|
||||
clientOperationService.deregisterInstance(service, instance, meta.getConnectionId());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.remote.handler.a2a;
|
||||
|
||||
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.remote.request.RequestMeta;
|
||||
import com.alibaba.nacos.auth.annotation.Secured;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
|
||||
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
|
||||
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
|
||||
import com.alibaba.nacos.core.remote.RequestHandler;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
|
||||
import com.alibaba.nacos.plugin.auth.constant.SignType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Nacos AI module query agent card request handler.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
@Component
|
||||
public class QueryAgentCardRequestHandler extends RequestHandler<QueryAgentCardRequest, QueryAgentCardResponse> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(QueryAgentCardRequestHandler.class);
|
||||
|
||||
private final A2aServerOperationService a2aServerOperationService;
|
||||
|
||||
public QueryAgentCardRequestHandler(A2aServerOperationService a2aServerOperationService) {
|
||||
this.a2aServerOperationService = a2aServerOperationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NamespaceValidation
|
||||
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.READ, signType = SignType.AI)
|
||||
public QueryAgentCardResponse handle(QueryAgentCardRequest request, RequestMeta meta) throws NacosException {
|
||||
AgentRequestUtil.fillNamespaceId(request);
|
||||
if (StringUtils.isBlank(request.getAgentName())) {
|
||||
QueryAgentCardResponse errorResponse = new QueryAgentCardResponse();
|
||||
errorResponse.setErrorInfo(NacosException.INVALID_PARAM, "parameters `agentName` can't be empty or null");
|
||||
return errorResponse;
|
||||
}
|
||||
return doHandler(request);
|
||||
}
|
||||
|
||||
private QueryAgentCardResponse doHandler(QueryAgentCardRequest request) {
|
||||
QueryAgentCardResponse response = new QueryAgentCardResponse();
|
||||
try {
|
||||
AgentCardDetailInfo result = a2aServerOperationService.getAgentCard(request.getNamespaceId(),
|
||||
request.getAgentName(), request.getVersion(), request.getRegistrationType());
|
||||
response.setAgentCardDetailInfo(result);
|
||||
} catch (NacosException e) {
|
||||
LOGGER.error("Query agent card for agent {} error: {}", request.getAgentName(), e.getErrMsg());
|
||||
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.remote.handler.a2a;
|
||||
|
||||
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
|
||||
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.exception.api.NacosApiException;
|
||||
import com.alibaba.nacos.api.model.v2.ErrorCode;
|
||||
import com.alibaba.nacos.api.remote.request.RequestMeta;
|
||||
import com.alibaba.nacos.auth.annotation.Secured;
|
||||
import com.alibaba.nacos.common.utils.JacksonUtils;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.core.namespace.filter.NamespaceValidation;
|
||||
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
|
||||
import com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor;
|
||||
import com.alibaba.nacos.core.remote.RequestHandler;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
|
||||
import com.alibaba.nacos.plugin.auth.constant.SignType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Nacos AI module release agent card request handler.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
@Component
|
||||
public class ReleaseAgentCardRequestHandler extends RequestHandler<ReleaseAgentCardRequest, ReleaseAgentCardResponse> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ReleaseAgentCardRequestHandler.class);
|
||||
|
||||
private final A2aServerOperationService a2aServerOperationService;
|
||||
|
||||
public ReleaseAgentCardRequestHandler(A2aServerOperationService a2aServerOperationService) {
|
||||
this.a2aServerOperationService = a2aServerOperationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NamespaceValidation
|
||||
@ExtractorManager.Extractor(rpcExtractor = AgentRequestParamExtractor.class)
|
||||
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
|
||||
public ReleaseAgentCardResponse handle(ReleaseAgentCardRequest request, RequestMeta meta) throws NacosException {
|
||||
AgentRequestUtil.fillNamespaceId(request);
|
||||
ReleaseAgentCardResponse response = new ReleaseAgentCardResponse();
|
||||
try {
|
||||
validateRequest(request);
|
||||
doHandler(request, meta);
|
||||
return response;
|
||||
} catch (NacosException e) {
|
||||
response.setErrorInfo(e.getErrCode(), e.getErrMsg());
|
||||
LOGGER.error("[{}] Release agent card {} error: {}", meta.getConnectionId(),
|
||||
null == request.getAgentCard() ? null : JacksonUtils.toJson(request.getAgentCard()), e.getErrMsg());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private void validateRequest(ReleaseAgentCardRequest request) throws NacosApiException {
|
||||
if (null == request.getAgentCard()) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `agentCard` can't be null");
|
||||
}
|
||||
AgentRequestUtil.validateAgentCard(request.getAgentCard());
|
||||
}
|
||||
|
||||
private void doHandler(ReleaseAgentCardRequest request, RequestMeta meta) throws NacosException {
|
||||
String namespaceId = request.getNamespaceId();
|
||||
AgentCard agentCard = request.getAgentCard();
|
||||
LOGGER.info("Release new agent {}, version {} into namespaceId {} from connectionId {}.", agentCard.getName(),
|
||||
agentCard.getVersion(), namespaceId, meta.getConnectionId());
|
||||
try {
|
||||
AgentCardDetailInfo existAgentCard = a2aServerOperationService.getAgentCard(namespaceId,
|
||||
agentCard.getName(), agentCard.getVersion(), StringUtils.EMPTY);
|
||||
LOGGER.info("AgentCard {} and target version {} already exist.", existAgentCard.getName(),
|
||||
existAgentCard.getVersion());
|
||||
} catch (NacosApiException e) {
|
||||
if (ErrorCode.AGENT_NOT_FOUND.getCode() == e.getDetailErrCode()) {
|
||||
// agent card not found, create new agent card.
|
||||
createAgentCard(namespaceId, agentCard, request.getRegistrationType());
|
||||
LOGGER.info("AgentCard {} released.", agentCard.getName());
|
||||
} else if (ErrorCode.AGENT_VERSION_NOT_FOUND.getCode() == e.getDetailErrCode()) {
|
||||
// agent card found but version not found, update agent card.
|
||||
createNewVersionAgentCard(namespaceId, agentCard, request.getRegistrationType(), request.isSetAsLatest());
|
||||
LOGGER.info("AgentCard {} new version {} released.", agentCard.getName(), agentCard.getVersion());
|
||||
} else {
|
||||
LOGGER.error("AgentCard {} released failed.", agentCard.getName(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createAgentCard(String namespaceId, AgentCard agentCard, String registrationType)
|
||||
throws NacosException {
|
||||
a2aServerOperationService.registerAgent(agentCard, namespaceId, registrationType);
|
||||
}
|
||||
|
||||
private void createNewVersionAgentCard(String namespaceId, AgentCard agentCard, String registrationType,
|
||||
boolean setAsLatest) throws NacosException {
|
||||
a2aServerOperationService.updateAgentCard(agentCard, namespaceId, registrationType, setAsLatest);
|
||||
}
|
||||
}
|
|
@ -12,25 +12,26 @@
|
|||
* 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 com.alibaba.nacos.ai.service;
|
||||
package com.alibaba.nacos.ai.service.a2a;
|
||||
|
||||
import com.alibaba.nacos.ai.constant.Constants;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentDetailForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentUpdateForm;
|
||||
import com.alibaba.nacos.ai.service.SyncEffectService;
|
||||
import com.alibaba.nacos.ai.service.a2a.identity.AgentIdCodecHolder;
|
||||
import com.alibaba.nacos.ai.utils.AgentCardUtil;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentInterface;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentVersionDetail;
|
||||
import com.alibaba.nacos.api.config.ConfigType;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.exception.api.NacosApiException;
|
||||
import com.alibaba.nacos.api.model.Page;
|
||||
import com.alibaba.nacos.api.model.v2.ErrorCode;
|
||||
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
|
||||
import com.alibaba.nacos.common.utils.JacksonUtils;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.config.server.model.ConfigInfo;
|
||||
|
@ -41,10 +42,12 @@ import com.alibaba.nacos.config.server.service.ConfigOperationService;
|
|||
import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService;
|
||||
import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest;
|
||||
import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse;
|
||||
import com.alibaba.nacos.config.server.utils.ParamUtils;
|
||||
import com.alibaba.nacos.naming.core.v2.index.ServiceStorage;
|
||||
import com.alibaba.nacos.naming.core.v2.pojo.Service;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static com.alibaba.nacos.ai.constant.Constants.A2A.AGENT_GROUP;
|
||||
import static com.alibaba.nacos.ai.constant.Constants.A2A.AGENT_VERSION_GROUP;
|
||||
|
@ -65,24 +68,20 @@ public class A2aServerOperationService {
|
|||
|
||||
private final SyncEffectService syncEffectService;
|
||||
|
||||
private final ServiceStorage serviceStorage;
|
||||
|
||||
private final AgentIdCodecHolder agentIdCodecHolder;
|
||||
|
||||
public A2aServerOperationService(ConfigQueryChainService configQueryChainService,
|
||||
ConfigOperationService configOperationService, ConfigDetailService configDetailService,
|
||||
SyncEffectService syncEffectService) {
|
||||
SyncEffectService syncEffectService, ServiceStorage serviceStorage,
|
||||
AgentIdCodecHolder agentIdCodecHolder) {
|
||||
this.configQueryChainService = configQueryChainService;
|
||||
this.configOperationService = configOperationService;
|
||||
this.configDetailService = configDetailService;
|
||||
this.syncEffectService = syncEffectService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register agent.
|
||||
*
|
||||
* @param form agent detail form
|
||||
* @throws NacosException nacos exception
|
||||
*/
|
||||
public void registerAgent(AgentDetailForm form) throws NacosException {
|
||||
AgentCard agentCard = AgentCardUtil.buildAgentCard(form);
|
||||
registerAgent(agentCard, form.getNamespaceId(), form.getRegistrationType());
|
||||
this.serviceStorage = serviceStorage;
|
||||
this.agentIdCodecHolder = agentIdCodecHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,7 +119,7 @@ public class A2aServerOperationService {
|
|||
* @throws NacosException nacos exception
|
||||
*/
|
||||
public void deleteAgent(String namespaceId, String agentName, String version) throws NacosException {
|
||||
String encodedName = ParamUtils.encodeName(agentName);
|
||||
String encodedName = agentIdCodecHolder.encode(agentName);
|
||||
|
||||
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(encodedName, AGENT_GROUP,
|
||||
namespaceId);
|
||||
|
@ -173,17 +172,6 @@ public class A2aServerOperationService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update agent card.
|
||||
*
|
||||
* @param form agent update form
|
||||
* @throws NacosException nacos exception
|
||||
*/
|
||||
public void updateAgentCard(AgentUpdateForm form) throws NacosException {
|
||||
AgentCard agentCard = AgentCardUtil.buildAgentCard(form);
|
||||
updateAgentCard(agentCard, form.getNamespaceId(), form.getRegistrationType(), form.getSetAsLatest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update agent card.
|
||||
*
|
||||
|
@ -256,7 +244,7 @@ public class A2aServerOperationService {
|
|||
*/
|
||||
public Page<AgentCardVersionInfo> listAgents(String namespaceId, String agentName, String search, int pageNo,
|
||||
int pageSize) throws NacosException {
|
||||
String encodedName = ParamUtils.encodeName(agentName);
|
||||
String encodedName = agentIdCodecHolder.encode(agentName);
|
||||
|
||||
String dataId;
|
||||
if (StringUtils.isEmpty(encodedName) || Constants.A2A.SEARCH_BLUR.equalsIgnoreCase(search)) {
|
||||
|
@ -299,29 +287,30 @@ public class A2aServerOperationService {
|
|||
* @param namespaceId namespaceId of agent
|
||||
* @param agentName agent name
|
||||
* @param version target version of want to query, if is null or empty, get latest version
|
||||
* @param registrationType registration type
|
||||
* @return agent card detail info
|
||||
* @throws NacosApiException nacos api exception
|
||||
*/
|
||||
public AgentCardDetailInfo getAgentCard(String namespaceId, String agentName, String version)
|
||||
throws NacosApiException {
|
||||
public AgentCardDetailInfo getAgentCard(String namespaceId, String agentName, String version,
|
||||
String registrationType) throws NacosApiException {
|
||||
AgentCardVersionInfo agentCardVersionInfo = queryAgentCardVersionInfo(namespaceId, agentName);
|
||||
return StringUtils.isEmpty(version) ? queryLatestVersion(agentCardVersionInfo, namespaceId)
|
||||
: queryTargetVersion(agentCardVersionInfo, version, namespaceId);
|
||||
return StringUtils.isEmpty(version) ? queryLatestVersion(agentCardVersionInfo, namespaceId, registrationType)
|
||||
: queryTargetVersion(agentCardVersionInfo, version, namespaceId, registrationType);
|
||||
}
|
||||
|
||||
private AgentCardDetailInfo queryLatestVersion(AgentCardVersionInfo agentCardVersionInfo, String namespaceId)
|
||||
throws NacosApiException {
|
||||
private AgentCardDetailInfo queryLatestVersion(AgentCardVersionInfo agentCardVersionInfo, String namespaceId,
|
||||
String registrationType) throws NacosApiException {
|
||||
String latestVersion = agentCardVersionInfo.getVersionDetails().stream().filter(AgentVersionDetail::isLatest)
|
||||
.findFirst().orElseThrow(
|
||||
() -> new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_VERSION_NOT_FOUND,
|
||||
String.format("Agent %s latest version not found", agentCardVersionInfo.getName())))
|
||||
.getVersion();
|
||||
return queryTargetVersion(agentCardVersionInfo, latestVersion, namespaceId);
|
||||
return queryTargetVersion(agentCardVersionInfo, latestVersion, namespaceId, registrationType);
|
||||
}
|
||||
|
||||
private AgentCardDetailInfo queryTargetVersion(AgentCardVersionInfo agentCardVersionInfo, String version,
|
||||
String namespaceId) throws NacosApiException {
|
||||
String versionDataId = ParamUtils.encodeName(agentCardVersionInfo.getName()) + "-" + version;
|
||||
String namespaceId, String registrationType) throws NacosApiException {
|
||||
String versionDataId = agentIdCodecHolder.encode(agentCardVersionInfo.getName()) + "-" + version;
|
||||
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(versionDataId,
|
||||
AGENT_VERSION_GROUP, namespaceId);
|
||||
ConfigQueryChainResponse response = configQueryChainService.handle(request);
|
||||
|
@ -329,12 +318,45 @@ public class A2aServerOperationService {
|
|||
throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_VERSION_NOT_FOUND,
|
||||
String.format("Agent %s version %s not found.", agentCardVersionInfo.getName(), version));
|
||||
}
|
||||
return JacksonUtils.toObj(response.getContent(), AgentCardDetailInfo.class);
|
||||
AgentCardDetailInfo result = JacksonUtils.toObj(response.getContent(), AgentCardDetailInfo.class);
|
||||
if (StringUtils.isBlank(registrationType)) {
|
||||
registrationType = result.getRegistrationType();
|
||||
}
|
||||
if (AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE.equalsIgnoreCase(registrationType)) {
|
||||
injectEndpoint(result, namespaceId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void injectEndpoint(AgentCardDetailInfo agentCard, String namespaceId) {
|
||||
String serviceName = agentIdCodecHolder.encode(agentCard.getName()) + "::" + agentCard.getVersion();
|
||||
Service service = Service.newService(namespaceId, Constants.A2A.AGENT_ENDPOINT_GROUP, serviceName);
|
||||
ServiceInfo serviceInfo = serviceStorage.getData(service);
|
||||
if (serviceInfo.getHosts().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<AgentInterface> allAgentEndpoints = serviceInfo.getHosts().stream().map(AgentCardUtil::buildAgentInterface)
|
||||
.toList();
|
||||
agentCard.setAdditionalInterfaces(allAgentEndpoints);
|
||||
List<AgentInterface> matchTransportEndpoints = allAgentEndpoints.stream()
|
||||
.filter(agentInterface -> agentInterface.getTransport()
|
||||
.equalsIgnoreCase(agentCard.getPreferredTransport())).toList();
|
||||
AgentInterface randomPreferredTransportEndpoint = randomOne(
|
||||
matchTransportEndpoints.isEmpty() ? allAgentEndpoints : matchTransportEndpoints);
|
||||
agentCard.setUrl(randomPreferredTransportEndpoint.getUrl());
|
||||
agentCard.setPreferredTransport(randomPreferredTransportEndpoint.getTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO abstract a choose policy.
|
||||
*/
|
||||
private AgentInterface randomOne(List<AgentInterface> agentInterfaces) {
|
||||
return agentInterfaces.get(ThreadLocalRandom.current().nextInt(agentInterfaces.size()));
|
||||
}
|
||||
|
||||
private ConfigForm transferVersionInfoToConfigForm(AgentCardVersionInfo agentCardVersionInfo, String namespaceId) {
|
||||
ConfigForm configForm = new ConfigForm();
|
||||
String actualDataId = ParamUtils.encodeName(agentCardVersionInfo.getName());
|
||||
String actualDataId = agentIdCodecHolder.encode(agentCardVersionInfo.getName());
|
||||
configForm.setDataId(actualDataId);
|
||||
configForm.setGroup(AGENT_GROUP);
|
||||
configForm.setNamespaceId(namespaceId);
|
||||
|
@ -349,7 +371,7 @@ public class A2aServerOperationService {
|
|||
|
||||
private ConfigForm transferAgentInfoToConfigForm(AgentCardDetailInfo storageInfo, String namespaceId) {
|
||||
ConfigForm configForm = new ConfigForm();
|
||||
String actualDataId = ParamUtils.encodeName(storageInfo.getName()) + "-" + storageInfo.getVersion();
|
||||
String actualDataId = agentIdCodecHolder.encode(storageInfo.getName()) + "-" + storageInfo.getVersion();
|
||||
configForm.setDataId(actualDataId);
|
||||
configForm.setGroup(AGENT_VERSION_GROUP);
|
||||
configForm.setNamespaceId(namespaceId);
|
||||
|
@ -362,19 +384,15 @@ public class A2aServerOperationService {
|
|||
return configForm;
|
||||
}
|
||||
|
||||
private AgentCardVersionInfo queryAgentCardVersionInfo(AgentForm form) throws NacosApiException {
|
||||
return queryAgentCardVersionInfo(form.getNamespaceId(), form.getName());
|
||||
}
|
||||
|
||||
private AgentCardVersionInfo queryAgentCardVersionInfo(String namespaceId, String name) throws NacosApiException {
|
||||
// Check if the agent exists
|
||||
String actualDataId = ParamUtils.encodeName(name);
|
||||
String actualDataId = agentIdCodecHolder.encode(name);
|
||||
ConfigQueryChainRequest request = ConfigQueryChainRequest.buildConfigQueryChainRequest(actualDataId,
|
||||
AGENT_GROUP, namespaceId);
|
||||
ConfigQueryChainResponse response = configQueryChainService.handle(request);
|
||||
if (response.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND) {
|
||||
throw new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_NOT_FOUND,
|
||||
"Cannot update agent: Agent not found: " + name);
|
||||
"Agent not found: " + name);
|
||||
}
|
||||
return JacksonUtils.toObj(response.getContent(), AgentCardVersionInfo.class);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.service.a2a.identity;
|
||||
|
||||
/**
|
||||
* Nacos AI module A2A(Agent & AgentCard)identity Codec.
|
||||
*
|
||||
* <p>
|
||||
* Agent and AgentCard allow user custom agent name without limit for now, but no limit means out of control and might cause un-expected behavior.
|
||||
* So when storage in Nacos, it should be match some word limits.
|
||||
* We need to encode and decode agent name as the identity to do storage.
|
||||
* </p>
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public interface AgentIdCodec {
|
||||
|
||||
/**
|
||||
* Encode agent name to identity.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @return identity encoded from agent name
|
||||
*/
|
||||
String encode(String agentName);
|
||||
|
||||
/**
|
||||
* Decode agent id to agent name.
|
||||
*
|
||||
* @param agentId agent identity
|
||||
* @return agent name
|
||||
*/
|
||||
String decode(String agentId);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.service.a2a.identity;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* The Holder of {@link AgentIdCodec}.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
@Component
|
||||
public class AgentIdCodecHolder {
|
||||
|
||||
private final AgentIdCodec agentIdCodec;
|
||||
|
||||
public AgentIdCodecHolder(ObjectProvider<AgentIdCodec> agentIdCodecsProvider) {
|
||||
this.agentIdCodec = agentIdCodecsProvider.getIfAvailable(AsciiAgentIdCodec::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode agent name to identity.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @return identity encoded from agent name
|
||||
*/
|
||||
public String encode(String agentName) {
|
||||
return agentIdCodec.encode(agentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode agent id to agent name.
|
||||
*
|
||||
* @param agentId agent identity
|
||||
* @return agent name
|
||||
*/
|
||||
public String decode(String agentId) {
|
||||
return agentIdCodec.decode(agentId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.service.a2a.identity;
|
||||
|
||||
import com.alibaba.nacos.config.server.utils.ParamUtils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent Identity Codec implement by ASCII.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AsciiAgentIdCodec implements AgentIdCodec {
|
||||
|
||||
private static final String ENCODE_PREFIX = "_-.SYSENC:";
|
||||
|
||||
private static final char ENCODE_MARK_CHAR = '_';
|
||||
|
||||
/**
|
||||
* Come From {@link ParamUtils#validChars} and remove {@link #ENCODE_MARK_CHAR}.
|
||||
*/
|
||||
private static final Set<Character> VALID_CHAR = Set.of('-', '.', ':');
|
||||
|
||||
@Override
|
||||
public String encode(String agentName) {
|
||||
if (ParamUtils.isValid(agentName)) {
|
||||
return agentName;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(ENCODE_PREFIX);
|
||||
for (char ch : agentName.toCharArray()) {
|
||||
if (Character.isLetterOrDigit(ch) || VALID_CHAR.contains(ch)) {
|
||||
// Keep letters, numbers, valid characters and non-underscores
|
||||
sb.append(ch);
|
||||
} else {
|
||||
sb.append(ENCODE_MARK_CHAR).append(String.format("%04x", (int) ch));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decode(String agentId) {
|
||||
if (!isEncoded(agentId)) {
|
||||
return agentId;
|
||||
}
|
||||
|
||||
String body = agentId.substring(ENCODE_PREFIX.length());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < body.length(); ) {
|
||||
char ch = body.charAt(i);
|
||||
if (ch == '_' && i + 5 <= body.length()) {
|
||||
String hexPart = body.substring(i + 1, i + 5);
|
||||
if (isHex(hexPart)) {
|
||||
try {
|
||||
int codePoint = Integer.parseInt(hexPart, 16);
|
||||
sb.append((char) codePoint);
|
||||
i += 5;
|
||||
continue;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("invalid encoded name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.append(ch);
|
||||
i++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private boolean isHex(String s) {
|
||||
for (char c : s.toCharArray()) {
|
||||
if (!Character.isDigit(c) && !isHexLetter(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return s.length() == 4;
|
||||
}
|
||||
|
||||
private boolean isHexLetter(char c) {
|
||||
return (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
|
||||
private boolean isEncoded(String name) {
|
||||
return name != null && name.startsWith(ENCODE_PREFIX);
|
||||
}
|
||||
}
|
|
@ -17,11 +17,13 @@
|
|||
package com.alibaba.nacos.ai.utils;
|
||||
|
||||
import com.alibaba.nacos.ai.constant.Constants;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentDetailForm;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentInterface;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentVersionDetail;
|
||||
import com.alibaba.nacos.api.naming.pojo.Instance;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
@ -35,30 +37,7 @@ import java.util.Collections;
|
|||
*/
|
||||
public class AgentCardUtil {
|
||||
|
||||
/**
|
||||
* Build Agent Card from Agent Detail form.
|
||||
*
|
||||
* @param form agent detail form
|
||||
* @return Agent Card
|
||||
*/
|
||||
public static AgentCard buildAgentCard(AgentDetailForm form) {
|
||||
AgentCard agentCard = new AgentCard();
|
||||
injectAgentCardInfo(agentCard, form);
|
||||
return agentCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Agent Card Storage Info from Agent Detail form.
|
||||
*
|
||||
* @param form agent detail form
|
||||
* @return Agent Card Storage Info
|
||||
*/
|
||||
public static AgentCardDetailInfo buildAgentCardDetailInfo(AgentDetailForm form) {
|
||||
AgentCardDetailInfo agentCardDetailInfo = new AgentCardDetailInfo();
|
||||
injectAgentCardInfo(agentCardDetailInfo, form);
|
||||
agentCardDetailInfo.setRegistrationType(form.getRegistrationType());
|
||||
return agentCardDetailInfo;
|
||||
}
|
||||
private static final String AGENT_INTERFACE_URL_PATTERN = "%s://%s:%s";
|
||||
|
||||
/**
|
||||
* Build Agent Card Storage Info from Agent Detail form.
|
||||
|
@ -73,24 +52,6 @@ public class AgentCardUtil {
|
|||
return agentCardDetailInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Agent Card Storage Info from Agent Detail form.
|
||||
*
|
||||
* @param form agent detail form
|
||||
* @param isLatest is latest version
|
||||
* @return Agent Card Version Info
|
||||
*/
|
||||
public static AgentCardVersionInfo buildAgentCardVersionInfo(AgentDetailForm form, boolean isLatest) {
|
||||
AgentCardVersionInfo agentCardVersionInfo = new AgentCardVersionInfo();
|
||||
injectAgentCardInfo(agentCardVersionInfo, form);
|
||||
agentCardVersionInfo.setRegistrationType(form.getRegistrationType());
|
||||
if (isLatest) {
|
||||
agentCardVersionInfo.setLatestPublishedVersion(form.getVersion());
|
||||
}
|
||||
agentCardVersionInfo.setVersionDetails(Collections.singletonList(buildAgentVersionDetail(form, isLatest)));
|
||||
return agentCardVersionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Agent Card Storage Info from AgentCard.
|
||||
*
|
||||
|
@ -111,21 +72,6 @@ public class AgentCardUtil {
|
|||
return agentCardVersionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Agent version detail from Agent Detail form.
|
||||
*
|
||||
* @param form agent detail form
|
||||
* @return Agent Version Detail
|
||||
*/
|
||||
public static AgentVersionDetail buildAgentVersionDetail(AgentDetailForm form, boolean isLatest) {
|
||||
AgentVersionDetail agentVersionDetail = new AgentVersionDetail();
|
||||
agentVersionDetail.setCreatedAt(getCurrentTime());
|
||||
agentVersionDetail.setUpdatedAt(getCurrentTime());
|
||||
agentVersionDetail.setVersion(form.getVersion());
|
||||
agentVersionDetail.setLatest(isLatest);
|
||||
return agentVersionDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Agent version detail from Agent Detail form.
|
||||
*
|
||||
|
@ -150,32 +96,33 @@ public class AgentCardUtil {
|
|||
versionDetail.setUpdatedAt(getCurrentTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {@link AgentInterface} from service {@link Instance}.
|
||||
*
|
||||
* @param instance service instance.
|
||||
* @return agent interface (endpoint)
|
||||
*/
|
||||
public static AgentInterface buildAgentInterface(Instance instance) {
|
||||
AgentInterface agentInterface = new AgentInterface();
|
||||
boolean isSupportTls = Boolean.parseBoolean(
|
||||
instance.getMetadata().get(Constants.A2A.NACOS_AGENT_ENDPOINT_SUPPORT_TLS));
|
||||
String protocol = isSupportTls ? Constants.PROTOCOL_TYPE_HTTPS : Constants.PROTOCOL_TYPE_HTTP;
|
||||
String url = String.format(AGENT_INTERFACE_URL_PATTERN, protocol, instance.getIp(), instance.getPort());
|
||||
String path = instance.getMetadata().get(Constants.A2A.AGENT_ENDPOINT_PATH_KEY);
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
url += path.startsWith("/") ? path : "/" + path;
|
||||
}
|
||||
agentInterface.setUrl(url);
|
||||
agentInterface.setTransport(instance.getMetadata().get(Constants.A2A.AGENT_ENDPOINT_TRANSPORT_KEY));
|
||||
return agentInterface;
|
||||
}
|
||||
|
||||
private static String getCurrentTime() {
|
||||
ZonedDateTime currentTime = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Constants.RELEASE_DATE_FORMAT);
|
||||
return currentTime.format(formatter);
|
||||
}
|
||||
|
||||
private static void injectAgentCardInfo(AgentCard agentCard, AgentDetailForm form) {
|
||||
agentCard.setProtocolVersion(form.getProtocolVersion());
|
||||
agentCard.setName(form.getName());
|
||||
agentCard.setDescription(form.getDescription());
|
||||
agentCard.setUrl(form.getUrl());
|
||||
agentCard.setVersion(form.getVersion());
|
||||
agentCard.setPreferredTransport(form.getPreferredTransport());
|
||||
agentCard.setAdditionalInterfaces(form.getAdditionalInterfaces());
|
||||
agentCard.setIconUrl(form.getIconUrl());
|
||||
agentCard.setProvider(form.getProvider());
|
||||
agentCard.setCapabilities(form.getCapabilities());
|
||||
agentCard.setSecuritySchemes(form.getSecuritySchemes());
|
||||
agentCard.setSecurity(form.getSecurity());
|
||||
agentCard.setDefaultInputModes(form.getDefaultInputModes());
|
||||
agentCard.setDefaultOutputModes(form.getDefaultOutputModes());
|
||||
agentCard.setSkills(form.getSkills());
|
||||
agentCard.setSupportsAuthenticatedExtendedCard(form.getSupportsAuthenticatedExtendedCard());
|
||||
agentCard.setDocumentationUrl(form.getDocumentationUrl());
|
||||
}
|
||||
|
||||
private static void copyAgentCardInfo(AgentCard target, AgentCard source) {
|
||||
target.setProtocolVersion(source.getProtocolVersion());
|
||||
target.setName(source.getName());
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package com.alibaba.nacos.ai.utils;
|
||||
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.exception.api.NacosApiException;
|
||||
import com.alibaba.nacos.api.exception.runtime.NacosDeserializationException;
|
||||
|
@ -48,10 +50,7 @@ public class AgentRequestUtil {
|
|||
try {
|
||||
AgentCard result = JacksonUtils.toObj(agentCardForm.getAgentCard(), new TypeReference<>() {
|
||||
});
|
||||
validateAgentCardField("name", result.getName());
|
||||
validateAgentCardField("description", result.getDescription());
|
||||
validateAgentCardField("version", result.getVersion());
|
||||
validateAgentCardField("protocolVersion", result.getProtocolVersion());
|
||||
validateAgentCard(result);
|
||||
return result;
|
||||
} catch (NacosDeserializationException e) {
|
||||
LOGGER.error(String.format("Deserialize %s from %s failed, ", AgentCard.class.getSimpleName(),
|
||||
|
@ -61,6 +60,29 @@ public class AgentRequestUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate agent card is legal.
|
||||
*
|
||||
* @param agentCard agent card
|
||||
* @throws NacosApiException if agent card is illegal.
|
||||
*/
|
||||
public static void validateAgentCard(AgentCard agentCard) throws NacosApiException {
|
||||
validateAgentCardField("name", agentCard.getName());
|
||||
validateAgentCardField("version", agentCard.getVersion());
|
||||
validateAgentCardField("protocolVersion", agentCard.getProtocolVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
|
||||
*
|
||||
* @param request agent request
|
||||
*/
|
||||
public static void fillNamespaceId(AbstractAgentRequest request) {
|
||||
if (StringUtils.isEmpty(request.getNamespaceId())) {
|
||||
request.setNamespaceId(AiConstants.A2a.A2A_DEFAULT_NAMESPACE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateAgentCardField(String fieldName, String fieldValue) throws NacosApiException {
|
||||
if (StringUtils.isEmpty(fieldValue)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
|
|||
import com.alibaba.nacos.api.ai.model.mcp.McpServiceRef;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpTool;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
|
||||
import com.alibaba.nacos.api.exception.api.NacosApiException;
|
||||
import com.alibaba.nacos.api.exception.runtime.NacosDeserializationException;
|
||||
import com.alibaba.nacos.api.model.v2.ErrorCode;
|
||||
|
@ -146,4 +147,15 @@ public class McpRequestUtil {
|
|||
}
|
||||
throw new IllegalArgumentException("input must be instance of McpServiceRef or Map");
|
||||
}
|
||||
|
||||
/**
|
||||
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
|
||||
*
|
||||
* @param request mcp request
|
||||
*/
|
||||
public static void fillNamespaceId(AbstractMcpRequest request) {
|
||||
if (StringUtils.isEmpty(request.getNamespaceId())) {
|
||||
request.setNamespaceId(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,4 +14,5 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
com.alibaba.nacos.ai.param.McpHttpParamExtractor
|
||||
com.alibaba.nacos.ai.param.McpHttpParamExtractor
|
||||
com.alibaba.nacos.ai.param.AgentHttpParamExtractor
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.ai.service.a2a.identity;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class AsciiAgentIdCodecTest {
|
||||
|
||||
private AsciiAgentIdCodec agentIdCodec;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
agentIdCodec = new AsciiAgentIdCodec();
|
||||
}
|
||||
|
||||
// Does not encode when name is already valid
|
||||
@Test
|
||||
void testUsageDoesNotEncodeValidNames() {
|
||||
String input = "abc123";
|
||||
assertEquals(input, agentIdCodec.encode(input));
|
||||
}
|
||||
|
||||
// Round-trip encode/decode when input contains a space
|
||||
@Test
|
||||
void testEncodeAndDecodeWhenNameContainsSpace() {
|
||||
String input = "hello world";
|
||||
String encoded = agentIdCodec.encode(input);
|
||||
assertEquals("_-.SYSENC:hello_0020world", encoded);
|
||||
assertEquals(input, agentIdCodec.decode(encoded));
|
||||
}
|
||||
|
||||
// Valid special characters (._:-) should be preserved without encoding
|
||||
@Test
|
||||
void testValidSpecialCharsAreKept() {
|
||||
String input = "name_ok.1:2";
|
||||
assertEquals(input, agentIdCodec.encode(input));
|
||||
}
|
||||
|
||||
// Round-trip encode/decode for mixed unicode letters and ASCII
|
||||
@Test
|
||||
void testRoundTripUnicodeChars() {
|
||||
String input = " Ω test";
|
||||
String encoded = agentIdCodec.encode(input);
|
||||
assertEquals("_-.SYSENC:_0020Ω_0020test", encoded);
|
||||
String decoded = agentIdCodec.decode(encoded);
|
||||
assertEquals(input, decoded);
|
||||
}
|
||||
|
||||
// Input starts with underscore followed by hex-like sequence; verify behavior policy
|
||||
@Test
|
||||
void testUnderscoreFollowedByHexAmbiguityHandledByPolicy() {
|
||||
String original = "1 _abcd";
|
||||
String encoded = agentIdCodec.encode(original);
|
||||
assertEquals("_-.SYSENC:1_0020_005fabcd", encoded);
|
||||
assertEquals(original, agentIdCodec.decode(encoded));
|
||||
}
|
||||
|
||||
// Round-trip for extreme boundary code points (NUL and U+FFFF)
|
||||
@Test
|
||||
void testBoundaryCharacters() {
|
||||
String input = "\u0000\uFFFF";
|
||||
String encoded = agentIdCodec.encode(input);
|
||||
assertEquals("_-.SYSENC:_0000_ffff", encoded);
|
||||
String decoded = agentIdCodec.decode(encoded);
|
||||
assertEquals(input, decoded);
|
||||
}
|
||||
|
||||
// Encoding keeps empty string as-is and preserves a single underscore
|
||||
@Test
|
||||
void testEncodeKeepsEmptyAndUnderscore() {
|
||||
String empty = "";
|
||||
String encodedEmpty = agentIdCodec.encode(empty);
|
||||
assertEquals("", encodedEmpty);
|
||||
|
||||
String underscoreOnly = "_";
|
||||
String encodedUnderscore = agentIdCodec.encode(underscoreOnly);
|
||||
assertEquals(underscoreOnly, encodedUnderscore);
|
||||
}
|
||||
|
||||
// Encoding is idempotent for already-encoded output; decode restores original
|
||||
@Test
|
||||
void testAlreadyEncodedStringIsIdempotentOnEncode() {
|
||||
String original = "with space and Ω and tab\t";
|
||||
String first = agentIdCodec.encode(original);
|
||||
String second = agentIdCodec.encode(first);
|
||||
assertEquals("_-.SYSENC:with_0020space_0020and_0020Ω_0020and_0020tab_0009", first);
|
||||
// encodeName should not double-encode an already valid string
|
||||
assertEquals(first, second);
|
||||
// decode should restore original
|
||||
assertEquals(original, agentIdCodec.decode(first));
|
||||
}
|
||||
|
||||
// Round-trip for mixture of ASCII, control (tab), and underscore suffix
|
||||
@Test
|
||||
void testMixedUnicodeAndControlCharactersRoundTrip() {
|
||||
String original = "A B\tC_";
|
||||
String encoded = agentIdCodec.encode(original);
|
||||
assertEquals("_-.SYSENC:A_0020B_0009C_005f", encoded);
|
||||
String decoded = agentIdCodec.decode(encoded);
|
||||
assertEquals(original, decoded);
|
||||
}
|
||||
|
||||
// Decoding a string with encoded prefix returns body; encoding leaves valid input unchanged
|
||||
@Test
|
||||
void testDecodeNameWithFakeEncodedPrefixBody() {
|
||||
String fake = "_-.SYSENC:hello";
|
||||
// This string is already valid; encodeName should return as-is
|
||||
assertEquals(fake, agentIdCodec.encode(fake));
|
||||
// decodeName should strip prefix and return body unchanged
|
||||
assertEquals("hello", agentIdCodec.decode(fake));
|
||||
}
|
||||
}
|
|
@ -27,10 +27,10 @@ class McpRequestUtilsTest {
|
|||
@Test
|
||||
void fillNamespaceId() {
|
||||
QueryMcpServerRequest request = new QueryMcpServerRequest();
|
||||
McpRequestUtils.fillNamespaceId(request);
|
||||
McpRequestUtil.fillNamespaceId(request);
|
||||
assertEquals(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, request.getNamespaceId());
|
||||
request.setNamespaceId("test");
|
||||
McpRequestUtils.fillNamespaceId(request);
|
||||
McpRequestUtil.fillNamespaceId(request);
|
||||
assertEquals("test", request.getNamespaceId());
|
||||
}
|
||||
}
|
|
@ -53,6 +53,12 @@ public enum AbilityKey {
|
|||
SERVER_MCP_REGISTRY("mcp", "Server whether support release mcp server and register endpoint for mcp server",
|
||||
AbilityMode.SERVER),
|
||||
|
||||
/**
|
||||
* For AI module Agent & Agent Card registry.
|
||||
*/
|
||||
SERVER_AGENT_REGISTRY("agent", "Server whether support release agent server and register endpoint for agent server",
|
||||
AbilityMode.SERVER),
|
||||
|
||||
/**
|
||||
* For fuzzy watch naming or config.
|
||||
*/
|
||||
|
@ -70,6 +76,12 @@ public enum AbilityKey {
|
|||
SDK_MCP_REGISTRY("mcp", "Client whether support release mcp server and register endpoint for mcp server",
|
||||
AbilityMode.SDK_CLIENT),
|
||||
|
||||
/**
|
||||
* For AI module Agent & Agent Card registry.
|
||||
*/
|
||||
SDK_AGENT_REGISTRY("agent", "Client whether support release agent server and register endpoint for agent server",
|
||||
AbilityMode.SDK_CLIENT),
|
||||
|
||||
/**
|
||||
* For Test temporarily.
|
||||
*/
|
||||
|
|
|
@ -48,6 +48,7 @@ public class SdkClientAbilities extends AbstractAbilityRegistry {
|
|||
supportedAbilities.put(AbilityKey.SDK_CLIENT_FUZZY_WATCH, true);
|
||||
supportedAbilities.put(AbilityKey.SDK_CLIENT_DISTRIBUTED_LOCK, true);
|
||||
supportedAbilities.put(AbilityKey.SDK_MCP_REGISTRY, true);
|
||||
supportedAbilities.put(AbilityKey.SDK_AGENT_REGISTRY, true);
|
||||
}
|
||||
|
||||
/**.
|
||||
|
|
|
@ -49,6 +49,7 @@ public class ServerAbilities extends AbstractAbilityRegistry {
|
|||
supportedAbilities.put(AbilityKey.SERVER_FUZZY_WATCH, true);
|
||||
supportedAbilities.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, true);
|
||||
supportedAbilities.put(AbilityKey.SERVER_MCP_REGISTRY, true);
|
||||
supportedAbilities.put(AbilityKey.SERVER_AGENT_REGISTRY, true);
|
||||
}
|
||||
|
||||
/**.
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai;
|
||||
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* Nacos AI A2A client service interface.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public interface A2aService {
|
||||
|
||||
/**
|
||||
* Get agent card with nacos extension detail with latest version.
|
||||
*
|
||||
* @param agentName name of agent card
|
||||
* @return agent card with nacos extension detail
|
||||
* @throws NacosException if request parameter is invalid or agent card not found or handle error
|
||||
*/
|
||||
default AgentCardDetailInfo getAgentCard(String agentName) throws NacosException {
|
||||
return getAgentCard(agentName, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent card with nacos extension detail with target version.
|
||||
*
|
||||
* @param agentName name of agent card
|
||||
* @param version target version, if null or empty, get latest version
|
||||
* @return agent card with nacos extension detail
|
||||
* @throws NacosException if request parameter is invalid or agent card not found or handle error
|
||||
*/
|
||||
default AgentCardDetailInfo getAgentCard(String agentName, String version) throws NacosException {
|
||||
return getAgentCard(agentName, version, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent card with nacos extension detail with target version.
|
||||
*
|
||||
* @param agentName name of agent card
|
||||
* @param version target version, if null or empty, get latest version
|
||||
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
|
||||
* default is empty, means use agent card setting in nacos.
|
||||
* @return agent card with nacos extension detail
|
||||
* @throws NacosException if request parameter is invalid or agent card not found or handle error
|
||||
*/
|
||||
AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException;
|
||||
|
||||
/**
|
||||
* Release new agent card or new version with default service type endpoint.
|
||||
*
|
||||
* <p>
|
||||
* If current agent card and version exist, This API will do nothing.
|
||||
* If current agent card exist but version not exist, This API will release new version.
|
||||
* If current t agent card not exist, This API will release new agent card.
|
||||
* </p>
|
||||
*
|
||||
* @param agentCard agent card need to release
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
default void releaseAgentCard(AgentCard agentCard) throws NacosException {
|
||||
releaseAgentCard(agentCard, AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release new agent card or new version.
|
||||
*
|
||||
* <p>
|
||||
* If current agent card and version exist, This API will do nothing.
|
||||
* If current agent card exist but version not exist, This API will release new version.
|
||||
* If current t agent card not exist, This API will release new agent card.
|
||||
* </p>
|
||||
*
|
||||
* @param agentCard agent card need to release
|
||||
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
default void releaseAgentCard(AgentCard agentCard, String registrationType) throws NacosException {
|
||||
releaseAgentCard(agentCard, registrationType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release new agent card or new version.
|
||||
*
|
||||
* <p>
|
||||
* If current agent card and version exist, This API will do nothing.
|
||||
* If current agent card exist but version not exist, This API will release new version.
|
||||
* If current t agent card not exist, This API will release new agent card.
|
||||
* </p>
|
||||
*
|
||||
* @param agentCard agent card need to release
|
||||
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
|
||||
* @param setAsLatest whether set new version as latest, default is false. This parameter is only effect when new version is released.
|
||||
* If current agent card not exist, whatever this parameter is, it will be set as latest.
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException;
|
||||
|
||||
/**
|
||||
* Register endpoint to agent card.
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param version version of this endpoint
|
||||
* @param address address for this endpoint
|
||||
* @param port port of this endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
default void registerAgentEndpoint(String agentName, String version, String address, int port)
|
||||
throws NacosException {
|
||||
registerAgentEndpoint(agentName, version, address, port, AiConstants.A2a.A2A_ENDPOINT_DEFAULT_TRANSPORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint to agent card.
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param version version of this endpoint
|
||||
* @param address address for this endpoint
|
||||
* @param port port of this endpoint
|
||||
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport)
|
||||
throws NacosException {
|
||||
registerAgentEndpoint(agentName, version, address, port, transport, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint to agent card.
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param version version of this endpoint
|
||||
* @param address address for this endpoint
|
||||
* @param port port of this endpoint
|
||||
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
|
||||
* @param path The path of endpoint request
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport,
|
||||
String path) throws NacosException {
|
||||
registerAgentEndpoint(agentName, version, address, port, transport, path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint to agent card.
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param version version of this endpoint
|
||||
* @param address address for this endpoint
|
||||
* @param port port of this endpoint
|
||||
* @param transport supported transport, according to A2A protocol, it should be `JSONRPC`, `GRPC` and `HTTP+JSON`
|
||||
* @param path The path of endpoint request
|
||||
* @param supportTls whether support tls
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
default void registerAgentEndpoint(String agentName, String version, String address, int port, String transport,
|
||||
String path, boolean supportTls) throws NacosException {
|
||||
AgentEndpoint agentEndpoint = new AgentEndpoint();
|
||||
agentEndpoint.setAddress(address);
|
||||
agentEndpoint.setPort(port);
|
||||
agentEndpoint.setTransport(transport);
|
||||
agentEndpoint.setPath(path);
|
||||
agentEndpoint.setSupportTls(supportTls);
|
||||
agentEndpoint.setVersion(version);
|
||||
registerAgentEndpoint(agentName, agentEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register endpoint to agent card.
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param endpoint endpoint info
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException;
|
||||
|
||||
/**
|
||||
* Deregister endpoint from agent card which registered by this client.
|
||||
*
|
||||
* <p>
|
||||
* Only endpoint registered by this client can be deregistered.
|
||||
* Other endpoint registered by other clients, call this API will no any effect.
|
||||
* </p>
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param version version of this endpoint
|
||||
* @param address address for this endpoint
|
||||
* @param port port of this endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
default void deregisterAgentEndpoint(String agentName, String version, String address, int port) throws NacosException {
|
||||
AgentEndpoint agentEndpoint = new AgentEndpoint();
|
||||
agentEndpoint.setAddress(address);
|
||||
agentEndpoint.setPort(port);
|
||||
agentEndpoint.setVersion(version);
|
||||
deregisterAgentEndpoint(agentName, agentEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister endpoint from agent card which registered by this client.
|
||||
*
|
||||
* <p>
|
||||
* Only endpoint registered by this client can be deregistered.
|
||||
* Other endpoint registered by other clients, call this API will no any effect.
|
||||
* </p>
|
||||
*
|
||||
* @param agentName name of agent
|
||||
* @param endpoint endpoint info
|
||||
* @throws NacosException if request parameter is invalid or handle error or agent not found
|
||||
*/
|
||||
void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException;
|
||||
}
|
|
@ -28,7 +28,7 @@ import com.alibaba.nacos.api.exception.NacosException;
|
|||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public interface AiService {
|
||||
public interface AiService extends A2aService {
|
||||
|
||||
/**
|
||||
* Get mcp server detail info for latest version.
|
||||
|
|
|
@ -63,5 +63,7 @@ public class AiConstants {
|
|||
* Default endpoint type using `backend` service of agent when discovery a2a agent.
|
||||
*/
|
||||
public static final String A2A_ENDPOINT_TYPE_SERVICE = "SERVICE";
|
||||
|
||||
public static final String A2A_ENDPOINT_DEFAULT_TRANSPORT = "JSONRPC";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.model.a2a;
|
||||
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.utils.StringUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Agent endpoint for A2A protocol.
|
||||
*
|
||||
* <p>
|
||||
* Details split version of {@link AgentInterface}.
|
||||
* </p>
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AgentEndpoint {
|
||||
|
||||
/**
|
||||
* Same with {@link AgentInterface#transport}, Default `JSONRPC`.
|
||||
*/
|
||||
private String transport = AiConstants.A2a.A2A_ENDPOINT_DEFAULT_TRANSPORT;
|
||||
|
||||
/**
|
||||
* Will be joined with {@link #port}, {@link #path}. Such as `<a href="http://address:port/path">...</a>`
|
||||
*/
|
||||
private String address;
|
||||
|
||||
private int port;
|
||||
|
||||
private String path = StringUtils.EMPTY;
|
||||
|
||||
/**
|
||||
* If {@code true}, the target {@link AgentInterface} should be `https`, otherwise should be `http`. Default {@code false}.
|
||||
*/
|
||||
private boolean supportTls;
|
||||
|
||||
private String version;
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getTransport() {
|
||||
return transport;
|
||||
}
|
||||
|
||||
public void setTransport(String transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean isSupportTls() {
|
||||
return supportTls;
|
||||
}
|
||||
|
||||
public void setSupportTls(boolean supportTls) {
|
||||
this.supportTls = supportTls;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only simple check address(IP or domain) and port.
|
||||
*
|
||||
* @param endpoint target endpoint
|
||||
* @return {@code true} if is equal, otherwise {@code false}
|
||||
*/
|
||||
public boolean simpleEquals(AgentEndpoint endpoint) {
|
||||
return Objects.equals(address, endpoint.address) && Objects.equals(port, endpoint.port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AgentEndpoint that = (AgentEndpoint) o;
|
||||
return port == that.port && supportTls == that.supportTls && Objects.equals(transport, that.transport)
|
||||
&& Objects.equals(address, that.address) && Objects.equals(path, that.path) && Objects.equals(version,
|
||||
that.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(transport, address, port, path, supportTls, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AgentEndpoint{" + "transport='" + transport + '\'' + ", address='" + address + '\'' + ", port=" + port
|
||||
+ ", path='" + path + '\'' + ", supportTls=" + supportTls + ", version='" + version + '\'' + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.request;
|
||||
|
||||
import com.alibaba.nacos.api.common.Constants;
|
||||
import com.alibaba.nacos.api.remote.request.Request;
|
||||
|
||||
/**
|
||||
* Nacos AI module agent request.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AbstractAgentRequest extends Request {
|
||||
|
||||
private String namespaceId;
|
||||
|
||||
private String agentName;
|
||||
|
||||
@Override
|
||||
public String getModule() {
|
||||
return Constants.AI.AI_MODULE;
|
||||
}
|
||||
|
||||
public String getNamespaceId() {
|
||||
return namespaceId;
|
||||
}
|
||||
|
||||
public void setNamespaceId(String namespaceId) {
|
||||
this.namespaceId = namespaceId;
|
||||
}
|
||||
|
||||
public String getAgentName() {
|
||||
return agentName;
|
||||
}
|
||||
|
||||
public void setAgentName(String agentName) {
|
||||
this.agentName = agentName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.request;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
|
||||
|
||||
/**
|
||||
* Register or Deregister endpoint for agent to nacos AI module request.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AgentEndpointRequest extends AbstractAgentRequest {
|
||||
|
||||
private AgentEndpoint endpoint;
|
||||
|
||||
/**
|
||||
* Should be {@link AiRemoteConstants#REGISTER_ENDPOINT} or {@link AiRemoteConstants#DE_REGISTER_ENDPOINT}.
|
||||
*/
|
||||
private String type;
|
||||
|
||||
public AgentEndpoint getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(AgentEndpoint endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.request;
|
||||
|
||||
/**
|
||||
* Nacos AI module query agent card request.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class QueryAgentCardRequest extends AbstractAgentRequest {
|
||||
|
||||
private String version;
|
||||
|
||||
private String registrationType;
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getRegistrationType() {
|
||||
return registrationType;
|
||||
}
|
||||
|
||||
public void setRegistrationType(String registrationType) {
|
||||
this.registrationType = registrationType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.request;
|
||||
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
|
||||
/**
|
||||
* Nacos AI module release new agent card or new version of exist agent card request.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class ReleaseAgentCardRequest extends AbstractAgentRequest {
|
||||
|
||||
private AgentCard agentCard;
|
||||
|
||||
private String registrationType = AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE;
|
||||
|
||||
private boolean setAsLatest;
|
||||
|
||||
public AgentCard getAgentCard() {
|
||||
return agentCard;
|
||||
}
|
||||
|
||||
public void setAgentCard(AgentCard agentCard) {
|
||||
this.agentCard = agentCard;
|
||||
}
|
||||
|
||||
public String getRegistrationType() {
|
||||
return registrationType;
|
||||
}
|
||||
|
||||
public void setRegistrationType(String registrationType) {
|
||||
this.registrationType = registrationType;
|
||||
}
|
||||
|
||||
public boolean isSetAsLatest() {
|
||||
return setAsLatest;
|
||||
}
|
||||
|
||||
public void setSetAsLatest(boolean setAsLatest) {
|
||||
this.setAsLatest = setAsLatest;
|
||||
}
|
||||
}
|
|
@ -14,27 +14,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.alibaba.nacos.ai.utils;
|
||||
package com.alibaba.nacos.api.ai.remote.response;
|
||||
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
|
||||
import com.alibaba.nacos.common.utils.StringUtils;
|
||||
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
|
||||
import com.alibaba.nacos.api.remote.response.Response;
|
||||
|
||||
/**
|
||||
* Nacos Mcp server request utils.
|
||||
* Register or Deregister endpoint for agent to nacos AI module response.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class McpRequestUtils {
|
||||
public class AgentEndpointResponse extends Response {
|
||||
|
||||
/**
|
||||
* If request contains valid namespaceId, do nothing. If not, fill default namespaceId.
|
||||
*
|
||||
* @param request mcp request
|
||||
* Should be {@link AiRemoteConstants#REGISTER_ENDPOINT} or {@link AiRemoteConstants#DE_REGISTER_ENDPOINT}.
|
||||
*/
|
||||
public static void fillNamespaceId(AbstractMcpRequest request) {
|
||||
if (StringUtils.isEmpty(request.getNamespaceId())) {
|
||||
request.setNamespaceId(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE);
|
||||
}
|
||||
private String type;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.response;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.remote.response.Response;
|
||||
|
||||
/**
|
||||
* Nacos AI module query agent card response.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class QueryAgentCardResponse extends Response {
|
||||
|
||||
private AgentCardDetailInfo agentCardDetailInfo;
|
||||
|
||||
public AgentCardDetailInfo getAgentCardDetailInfo() {
|
||||
return agentCardDetailInfo;
|
||||
}
|
||||
|
||||
public void setAgentCardDetailInfo(AgentCardDetailInfo agentCardDetailInfo) {
|
||||
this.agentCardDetailInfo = agentCardDetailInfo;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.api.ai.remote.response;
|
||||
|
||||
import com.alibaba.nacos.api.remote.response.Response;
|
||||
|
||||
/**
|
||||
* Nacos AI module release new agent card or new version of exist agent card response.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class ReleaseAgentCardResponse extends Response {
|
||||
}
|
|
@ -80,4 +80,11 @@ com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse
|
|||
com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest
|
||||
com.alibaba.nacos.api.ai.remote.response.ReleaseMcpServerResponse
|
||||
com.alibaba.nacos.api.ai.remote.request.McpServerEndpointRequest
|
||||
com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse
|
||||
com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse
|
||||
|
||||
com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest
|
||||
com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse
|
||||
com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest
|
||||
com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse
|
||||
com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest
|
||||
com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse
|
|
@ -17,6 +17,9 @@
|
|||
package com.alibaba.nacos.api.ai;
|
||||
|
||||
import com.alibaba.nacos.api.ai.listener.AbstractNacosMcpServerListener;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
|
||||
|
@ -75,6 +78,28 @@ class AiServiceDefaultMethodTest {
|
|||
invokeMark.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint)
|
||||
throws NacosException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint)
|
||||
throws NacosException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() throws NacosException {
|
||||
}
|
||||
|
|
|
@ -48,22 +48,26 @@ class AbilityKeyTest {
|
|||
enumMap.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, false);
|
||||
enumMap.put(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC, false);
|
||||
enumMap.put(AbilityKey.SERVER_MCP_REGISTRY, false);
|
||||
enumMap.put(AbilityKey.SERVER_AGENT_REGISTRY, false);
|
||||
stringBooleanMap = AbilityKey.mapStr(enumMap);
|
||||
assertEquals(4, stringBooleanMap.size());
|
||||
assertEquals(5, stringBooleanMap.size());
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_FUZZY_WATCH.getName()));
|
||||
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_DISTRIBUTED_LOCK.getName()));
|
||||
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC.getName()));
|
||||
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_MCP_REGISTRY.getName()));
|
||||
assertFalse(stringBooleanMap.get(AbilityKey.SERVER_AGENT_REGISTRY.getName()));
|
||||
|
||||
enumMap.put(AbilityKey.SERVER_DISTRIBUTED_LOCK, true);
|
||||
enumMap.put(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC, true);
|
||||
enumMap.put(AbilityKey.SERVER_MCP_REGISTRY, true);
|
||||
enumMap.put(AbilityKey.SERVER_AGENT_REGISTRY, true);
|
||||
stringBooleanMap = AbilityKey.mapStr(enumMap);
|
||||
assertEquals(4, stringBooleanMap.size());
|
||||
assertEquals(5, stringBooleanMap.size());
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_FUZZY_WATCH.getName()));
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_DISTRIBUTED_LOCK.getName()));
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_PERSISTENT_INSTANCE_BY_GRPC.getName()));
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_MCP_REGISTRY.getName()));
|
||||
assertTrue(stringBooleanMap.get(AbilityKey.SERVER_AGENT_REGISTRY.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -105,9 +109,9 @@ class AbilityKeyTest {
|
|||
@Test
|
||||
void testGetAllValues() {
|
||||
Collection<AbilityKey> actual = AbilityKey.getAllValues(AbilityMode.SERVER);
|
||||
assertEquals(4, actual.size());
|
||||
assertEquals(5, actual.size());
|
||||
actual = AbilityKey.getAllValues(AbilityMode.SDK_CLIENT);
|
||||
assertEquals(3, actual.size());
|
||||
assertEquals(4, actual.size());
|
||||
actual = AbilityKey.getAllValues(AbilityMode.CLUSTER_CLIENT);
|
||||
assertEquals(1, actual.size());
|
||||
}
|
||||
|
@ -115,9 +119,9 @@ class AbilityKeyTest {
|
|||
@Test
|
||||
void testGetAllNames() {
|
||||
Collection<String> actual = AbilityKey.getAllNames(AbilityMode.SERVER);
|
||||
assertEquals(4, actual.size());
|
||||
assertEquals(5, actual.size());
|
||||
actual = AbilityKey.getAllNames(AbilityMode.SDK_CLIENT);
|
||||
assertEquals(3, actual.size());
|
||||
assertEquals(4, actual.size());
|
||||
actual = AbilityKey.getAllNames(AbilityMode.CLUSTER_CLIENT);
|
||||
assertEquals(1, actual.size());
|
||||
}
|
||||
|
|
|
@ -18,8 +18,12 @@ package com.alibaba.nacos.client.ai;
|
|||
|
||||
import com.alibaba.nacos.api.PropertyKeyConst;
|
||||
import com.alibaba.nacos.api.ai.AiService;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.listener.AbstractNacosMcpServerListener;
|
||||
import com.alibaba.nacos.api.ai.listener.NacosMcpServerEvent;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
|
||||
|
@ -177,6 +181,73 @@ public class NacosAiService implements AiService {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType) throws NacosException {
|
||||
if (StringUtils.isBlank(agentName)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `agentName` can't be empty or null");
|
||||
}
|
||||
return grpcClient.getAgentCard(agentName, version, registrationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest) throws NacosException {
|
||||
if (null == agentCard) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `agentCard` can't be null");
|
||||
}
|
||||
validateAgentCardField("name", agentCard.getName());
|
||||
validateAgentCardField("version", agentCard.getVersion());
|
||||
validateAgentCardField("protocolVersion", agentCard.getProtocolVersion());
|
||||
if (StringUtils.isBlank(registrationType)) {
|
||||
registrationType = AiConstants.A2a.A2A_ENDPOINT_TYPE_SERVICE;
|
||||
}
|
||||
grpcClient.releaseAgentCard(agentCard, registrationType, setAsLatest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
|
||||
if (StringUtils.isBlank(agentName)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `agentName` can't be empty or null");
|
||||
}
|
||||
validateAgentEndpoint(endpoint);
|
||||
grpcClient.registerAgentEndpoint(agentName, endpoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint)
|
||||
throws NacosException {
|
||||
if (StringUtils.isBlank(agentName)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `agentName` can't be empty or null");
|
||||
}
|
||||
validateAgentEndpoint(endpoint);
|
||||
grpcClient.deregisterAgentEndpoint(agentName, endpoint);
|
||||
}
|
||||
|
||||
private void validateAgentEndpoint(AgentEndpoint endpoint) throws NacosApiException {
|
||||
if (null == endpoint) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"parameters `endpoint` can't be null");
|
||||
}
|
||||
if (StringUtils.isBlank(endpoint.getVersion())) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter `endpoint.version` can't be empty or null");
|
||||
}
|
||||
Instance instance = new Instance();
|
||||
instance.setIp(endpoint.getAddress());
|
||||
instance.setPort(endpoint.getPort());
|
||||
instance.validate();
|
||||
}
|
||||
|
||||
private static void validateAgentCardField(String fieldName, String fieldValue) throws NacosApiException {
|
||||
if (StringUtils.isEmpty(fieldValue)) {
|
||||
throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING,
|
||||
"Required parameter `agentCard." + fieldName + "` not present");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() throws NacosException {
|
||||
this.grpcClient.shutdown();
|
||||
|
|
|
@ -19,17 +19,27 @@ package com.alibaba.nacos.client.ai.remote;
|
|||
import com.alibaba.nacos.api.ability.constant.AbilityKey;
|
||||
import com.alibaba.nacos.api.ability.constant.AbilityStatus;
|
||||
import com.alibaba.nacos.api.ai.constant.AiConstants;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
|
||||
import com.alibaba.nacos.api.ai.remote.AiRemoteConstants;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.McpServerEndpointRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.QueryMcpServerRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse;
|
||||
import com.alibaba.nacos.api.ai.remote.response.McpServerEndpointResponse;
|
||||
import com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse;
|
||||
import com.alibaba.nacos.api.ai.remote.response.QueryMcpServerResponse;
|
||||
import com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse;
|
||||
import com.alibaba.nacos.api.ai.remote.response.ReleaseMcpServerResponse;
|
||||
import com.alibaba.nacos.api.common.Constants;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
|
@ -284,6 +294,130 @@ public class AiGrpcClient implements Closeable {
|
|||
mcpServerCacheHolder.removeMcpServerUpdateTask(mcpName, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent card with nacos extension detail with target version.
|
||||
*
|
||||
* @param agentName name of agent card
|
||||
* @param version target version, if null or empty, get latest version
|
||||
* @param registrationType registration type
|
||||
* @return agent card with nacos extension detail
|
||||
* @throws NacosException if request parameter is invalid or agent card not found or handle error
|
||||
*/
|
||||
public AgentCardDetailInfo getAgentCard(String agentName, String version, String registrationType)
|
||||
throws NacosException {
|
||||
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
|
||||
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
|
||||
"Request Nacos server version is too low, not support agent registry feature.");
|
||||
}
|
||||
QueryAgentCardRequest request = new QueryAgentCardRequest();
|
||||
request.setNamespaceId(this.namespaceId);
|
||||
request.setAgentName(agentName);
|
||||
request.setVersion(version);
|
||||
request.setRegistrationType(registrationType);
|
||||
QueryAgentCardResponse response = requestToServer(request, QueryAgentCardResponse.class);
|
||||
return response.getAgentCardDetailInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release new agent card or new version.
|
||||
*
|
||||
* <p>
|
||||
* If current agent card and version exist, This API will do nothing. If current agent card exist but version not
|
||||
* exist, This API will release new version. If current t agent card not exist, This API will release new agent
|
||||
* card.
|
||||
* </p>
|
||||
*
|
||||
* @param agentCard agent card need to release
|
||||
* @param registrationType {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_URL} or
|
||||
* {@link AiConstants.A2a#A2A_ENDPOINT_TYPE_SERVICE}
|
||||
* @param setAsLatest whether set new version as latest, default is false. This parameter is only effect when new version is released.
|
||||
* If current agent card not exist, whatever this parameter is, it will be set as latest.
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
public void releaseAgentCard(AgentCard agentCard, String registrationType, boolean setAsLatest)
|
||||
throws NacosException {
|
||||
LOGGER.info("[{}] Release Agent Card {}, version {}.", uuid, agentCard.getName(), agentCard.getVersion());
|
||||
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
|
||||
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
|
||||
"Request Nacos server version is too low, not support agent registry feature.");
|
||||
}
|
||||
ReleaseAgentCardRequest request = new ReleaseAgentCardRequest();
|
||||
request.setNamespaceId(this.namespaceId);
|
||||
request.setAgentName(agentCard.getName());
|
||||
request.setRegistrationType(registrationType);
|
||||
request.setAgentCard(agentCard);
|
||||
request.setSetAsLatest(setAsLatest);
|
||||
requestToServer(request, ReleaseAgentCardResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register agent endpoint into agent.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @param endpoint agent endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
public void registerAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
|
||||
LOGGER.info("[{}] REGISTER Agent endpoint {} into agent {}", uuid, endpoint.toString(), agentName);
|
||||
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
|
||||
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
|
||||
"Request Nacos server version is too low, not support agent registry feature.");
|
||||
}
|
||||
redoService.cachedAgentEndpointForRedo(agentName, endpoint);
|
||||
doRegisterAgentEndpoint(agentName, endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual do register agent endpoint into agent.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @param endpoint agent endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
public void doRegisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
|
||||
AgentEndpointRequest request = new AgentEndpointRequest();
|
||||
request.setNamespaceId(this.namespaceId);
|
||||
request.setAgentName(agentName);
|
||||
request.setType(AiRemoteConstants.REGISTER_ENDPOINT);
|
||||
request.setEndpoint(endpoint);
|
||||
requestToServer(request, AgentEndpointResponse.class);
|
||||
redoService.agentEndpointRegistered(agentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister agent endpoint from agent.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @param endpoint agent endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
public void deregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
|
||||
LOGGER.info("[{}] DE-REGISTER agent endpoint {} from agent {}", uuid, endpoint.toString(), agentName);
|
||||
if (!isAbilitySupportedByServer(AbilityKey.SERVER_AGENT_REGISTRY)) {
|
||||
throw new NacosRuntimeException(NacosException.SERVER_NOT_IMPLEMENTED,
|
||||
"Request Nacos server version is too low, not support agent registry feature.");
|
||||
}
|
||||
redoService.agentEndpointDeregister(agentName);
|
||||
doDeregisterAgentEndpoint(agentName, endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual do deregister agent endpoint from agent.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @param endpoint agent endpoint
|
||||
* @throws NacosException if request parameter is invalid or handle error
|
||||
*/
|
||||
public void doDeregisterAgentEndpoint(String agentName, AgentEndpoint endpoint) throws NacosException {
|
||||
AgentEndpointRequest request = new AgentEndpointRequest();
|
||||
request.setNamespaceId(this.namespaceId);
|
||||
request.setAgentName(agentName);
|
||||
request.setType(AiRemoteConstants.DE_REGISTER_ENDPOINT);
|
||||
request.setEndpoint(endpoint);
|
||||
requestToServer(request, AgentEndpointResponse.class);
|
||||
redoService.agentEndpointDeregistered(agentName);
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return rpcClient.isRunning();
|
||||
}
|
||||
|
@ -302,8 +436,11 @@ public class AiGrpcClient implements Closeable {
|
|||
Response response = null;
|
||||
try {
|
||||
if (request instanceof AbstractMcpRequest) {
|
||||
request.putAllHeader(getSecurityHeaders(((AbstractMcpRequest) request).getNamespaceId(),
|
||||
((AbstractMcpRequest) request).getMcpName()));
|
||||
AbstractMcpRequest mcpRequest = (AbstractMcpRequest) request;
|
||||
request.putAllHeader(getSecurityHeaders(mcpRequest.getNamespaceId(), mcpRequest.getMcpName()));
|
||||
} else if (request instanceof AbstractAgentRequest) {
|
||||
AbstractAgentRequest agentRequest = (AbstractAgentRequest) request;
|
||||
request.putAllHeader(getSecurityHeaders(agentRequest.getNamespaceId(), agentRequest.getAgentName()));
|
||||
} else {
|
||||
throw new NacosException(400,
|
||||
String.format("Unknown AI request type: %s", request.getClass().getSimpleName()));
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.client.ai.remote.redo;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.client.redo.data.RedoData;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Nacos AI module mcp server endpoint redo data.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AgentEndpointRedoData extends RedoData<AgentEndpoint> {
|
||||
|
||||
private final String agentName;
|
||||
|
||||
public AgentEndpointRedoData(String agentName, AgentEndpoint agentEndpoint) {
|
||||
this.agentName = agentName;
|
||||
this.set(agentEndpoint);
|
||||
}
|
||||
|
||||
public String getAgentName() {
|
||||
return agentName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
AgentEndpointRedoData that = (AgentEndpointRedoData) o;
|
||||
return Objects.equals(agentName, that.agentName) && super.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), agentName);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.alibaba.nacos.client.ai.remote.redo;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.remote.RemoteConstants;
|
||||
import com.alibaba.nacos.client.ai.remote.AiGrpcClient;
|
||||
import com.alibaba.nacos.client.env.NacosClientProperties;
|
||||
|
@ -90,4 +91,38 @@ public class AiGrpcRedoService extends AbstractRedoService {
|
|||
result.set(mcpServerEndpoint);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void cachedAgentEndpointForRedo(String agentName, AgentEndpoint agentEndpoint) {
|
||||
AgentEndpointRedoData redoData = new AgentEndpointRedoData(agentName, agentEndpoint);
|
||||
super.cachedRedoData(agentName, redoData, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public void removeAgentEndpointForRedo(String agentName) {
|
||||
super.removeRedoData(agentName, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public void agentEndpointRegistered(String agentName) {
|
||||
super.dataRegistered(agentName, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public void agentEndpointDeregister(String agentName) {
|
||||
super.dataDeregister(agentName, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public void agentEndpointDeregistered(String agentName) {
|
||||
super.dataDeregistered(agentName, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public boolean isAgentEndpointRegistered(String agentName) {
|
||||
return super.isDataRegistered(agentName, AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public Set<RedoData<AgentEndpoint>> findAgentEndpointRedoData() {
|
||||
return super.findRedoData(AgentEndpoint.class);
|
||||
}
|
||||
|
||||
public AgentEndpoint getAgentEndpoint(String agentName) {
|
||||
RedoData<AgentEndpoint> redoData = super.getRedoData(agentName, AgentEndpoint.class);
|
||||
return redoData == null ? null : redoData.get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.alibaba.nacos.client.ai.remote.redo;
|
||||
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.client.ai.remote.AiGrpcClient;
|
||||
import com.alibaba.nacos.client.naming.remote.gprc.redo.data.NamingRedoData;
|
||||
|
@ -44,16 +45,54 @@ public class AiRedoScheduledTask extends AbstractRedoTask<AiGrpcRedoService> {
|
|||
protected void redoData() throws NacosException {
|
||||
try {
|
||||
redoForMcpSeverEndpoint();
|
||||
redoForAgentEndpoint();
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Redo task run with unexpected exception: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void redoForAgentEndpoint() {
|
||||
for (RedoData<AgentEndpoint> each : getRedoService().findAgentEndpointRedoData()) {
|
||||
AgentEndpointRedoData redoData = (AgentEndpointRedoData) each;
|
||||
try {
|
||||
redoForAgentEndpoint(redoData);
|
||||
} catch (NacosException e) {
|
||||
LOGGER.error("Redo agent endpoint operation {} for {}} failed. ", each.getRedoType(),
|
||||
redoData.getAgentName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void redoForAgentEndpoint(AgentEndpointRedoData redoData) throws NacosException {
|
||||
NamingRedoData.RedoType redoType = redoData.getRedoType();
|
||||
String agentName = redoData.getAgentName();
|
||||
LOGGER.info("Redo agent endpoint operation {} for {}.", redoType, agentName);
|
||||
AgentEndpoint endpoint = redoData.get();
|
||||
switch (redoType) {
|
||||
case REGISTER:
|
||||
if (!aiGrpcClient.isEnable()) {
|
||||
return;
|
||||
}
|
||||
aiGrpcClient.doRegisterAgentEndpoint(agentName, endpoint);
|
||||
break;
|
||||
case UNREGISTER:
|
||||
if (!aiGrpcClient.isEnable()) {
|
||||
return;
|
||||
}
|
||||
aiGrpcClient.doDeregisterAgentEndpoint(agentName, endpoint);
|
||||
break;
|
||||
case REMOVE:
|
||||
getRedoService().removeMcpServerEndpointForRedo(agentName);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
private void redoForMcpSeverEndpoint() {
|
||||
for (RedoData<McpServerEndpoint> each : getRedoService().findMcpServerEndpointRedoData()) {
|
||||
McpServerEndpointRedoData redoData = (McpServerEndpointRedoData) each;
|
||||
try {
|
||||
redoForEndpoint(redoData);
|
||||
redoForMcpServerEndpoint(redoData);
|
||||
} catch (NacosException e) {
|
||||
LOGGER.error("Redo mcp server endpoint operation {} for {}} failed. ", each.getRedoType(),
|
||||
redoData.getMcpName(), e);
|
||||
|
@ -61,7 +100,7 @@ public class AiRedoScheduledTask extends AbstractRedoTask<AiGrpcRedoService> {
|
|||
}
|
||||
}
|
||||
|
||||
private void redoForEndpoint(McpServerEndpointRedoData redoData) throws NacosException {
|
||||
private void redoForMcpServerEndpoint(McpServerEndpointRedoData redoData) throws NacosException {
|
||||
NamingRedoData.RedoType redoType = redoData.getRedoType();
|
||||
String mcpName = redoData.getMcpName();
|
||||
LOGGER.info("Redo mcp server endpoint operation {} for {}.", redoType, mcpName);
|
||||
|
|
|
@ -41,7 +41,7 @@ class ClientAbilityControlManagerTest {
|
|||
assertEquals(1, actual.size());
|
||||
assertTrue(actual.containsKey(AbilityMode.SDK_CLIENT));
|
||||
// Current not define sdk ability.
|
||||
assertEquals(3, actual.get(AbilityMode.SDK_CLIENT).size());
|
||||
assertEquals(4, actual.get(AbilityMode.SDK_CLIENT).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -47,6 +47,8 @@ public class DefaultParamChecker extends AbstractParamChecker {
|
|||
|
||||
private Pattern mcpNamePattern;
|
||||
|
||||
private Pattern agentNamePattern;
|
||||
|
||||
private static final String CHECKER_TYPE = "default";
|
||||
|
||||
private static final String MAX_METADATA_LENGTH_PROP_NAME = "nacos.naming.service.metadata.length";
|
||||
|
@ -91,6 +93,7 @@ public class DefaultParamChecker extends AbstractParamChecker {
|
|||
this.clusterPattern = Pattern.compile(this.paramCheckRule.clusterPatternString);
|
||||
this.ipPattern = Pattern.compile(this.paramCheckRule.ipPatternString);
|
||||
this.mcpNamePattern = Pattern.compile(this.paramCheckRule.clusterPatternString);
|
||||
this.agentNamePattern = Pattern.compile(this.paramCheckRule.agentNamePatternString);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,6 +162,10 @@ public class DefaultParamChecker extends AbstractParamChecker {
|
|||
if (!paramCheckResponse.isSuccess()) {
|
||||
return paramCheckResponse;
|
||||
}
|
||||
paramCheckResponse = checkAgentNameFormat(paramInfo.getAgentName());
|
||||
if (!paramCheckResponse.isSuccess()) {
|
||||
return paramCheckResponse;
|
||||
}
|
||||
paramCheckResponse.setSuccess(true);
|
||||
return paramCheckResponse;
|
||||
}
|
||||
|
@ -438,7 +445,7 @@ public class DefaultParamChecker extends AbstractParamChecker {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check data id format.
|
||||
* Check mcp name format.
|
||||
*
|
||||
* @param mcpName the mcp name
|
||||
* @return the param check response
|
||||
|
@ -463,4 +470,31 @@ public class DefaultParamChecker extends AbstractParamChecker {
|
|||
paramCheckResponse.setSuccess(true);
|
||||
return paramCheckResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check agent name format.
|
||||
*
|
||||
* @param agentName agent name
|
||||
* @return the param check response
|
||||
*/
|
||||
public ParamCheckResponse checkAgentNameFormat(String agentName) {
|
||||
ParamCheckResponse paramCheckResponse = new ParamCheckResponse();
|
||||
if (StringUtils.isBlank(agentName)) {
|
||||
paramCheckResponse.setSuccess(true);
|
||||
return paramCheckResponse;
|
||||
}
|
||||
if (agentName.length() > paramCheckRule.maxAgentNameLength) {
|
||||
paramCheckResponse.setSuccess(false);
|
||||
paramCheckResponse.setMessage(
|
||||
String.format("Param 'agentName' is illegal, the param length should not exceed %d.", paramCheckRule.maxAgentNameLength));
|
||||
return paramCheckResponse;
|
||||
}
|
||||
if (!agentNamePattern.matcher(agentName).matches()) {
|
||||
paramCheckResponse.setSuccess(false);
|
||||
paramCheckResponse.setMessage("Param 'agentName' is illegal, illegal characters should not appear in the param.");
|
||||
return paramCheckResponse;
|
||||
}
|
||||
paramCheckResponse.setSuccess(true);
|
||||
return paramCheckResponse;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,5 +57,7 @@ public class ParamCheckRule {
|
|||
|
||||
public int maxMetadataLength = 1024;
|
||||
|
||||
|
||||
public String agentNamePatternString = "^[\\x20-\\x7E]+$";
|
||||
|
||||
public int maxAgentNameLength = 64;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ public class ParamInfo {
|
|||
|
||||
private String mcpId;
|
||||
|
||||
private String agentName;
|
||||
|
||||
public String getNamespaceShowName() {
|
||||
return namespaceShowName;
|
||||
}
|
||||
|
@ -143,4 +145,12 @@ public class ParamInfo {
|
|||
public void setMcpName(String mcpName) {
|
||||
this.mcpName = mcpName;
|
||||
}
|
||||
|
||||
public String getAgentName() {
|
||||
return agentName;
|
||||
}
|
||||
|
||||
public void setAgentName(String agentName) {
|
||||
this.agentName = agentName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,8 +51,6 @@ public class ParamUtils {
|
|||
|
||||
private static final String ENCRYPTED_DATA_KEY = "encryptedDataKey";
|
||||
|
||||
private static final String ENCODE_PREFIX = "_-.SYSENC:";
|
||||
|
||||
/**
|
||||
* Whitelist checks that valid parameters can only contain letters, Numbers, and characters in validChars, and
|
||||
* cannot be empty.
|
||||
|
@ -239,70 +237,4 @@ public class ParamUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* encode name.
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidComplexConditionRule")
|
||||
public static String encodeName(String name) {
|
||||
if (isValid(name)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(ENCODE_PREFIX);
|
||||
for (char ch : name.toCharArray()) {
|
||||
if (Character.isLetterOrDigit(ch) || (isValidChar(ch) && ch != '_')) {
|
||||
// Keep letters, numbers, valid characters and non-underscores
|
||||
sb.append(ch);
|
||||
} else {
|
||||
sb.append("_").append(String.format("%04x", (int) ch));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static boolean isEncoded(String name) {
|
||||
return name != null && name.startsWith(ENCODE_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* decode name.
|
||||
*/
|
||||
public static String decodeName(String encoded) {
|
||||
if (!isEncoded(encoded)) {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
String body = encoded.substring(ENCODE_PREFIX.length());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < body.length();) {
|
||||
char ch = body.charAt(i);
|
||||
if (ch == '_' && i + 5 <= body.length()) {
|
||||
String hexPart = body.substring(i + 1, i + 5);
|
||||
if (isHex(hexPart)) {
|
||||
try {
|
||||
int codePoint = Integer.parseInt(hexPart, 16);
|
||||
sb.append((char) codePoint);
|
||||
i += 5;
|
||||
continue;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("invalid encoded name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.append(ch);
|
||||
i++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.AvoidComplexConditionRule")
|
||||
private static boolean isHex(String s) {
|
||||
for (char c : s.toCharArray()) {
|
||||
if (!Character.isDigit(c) && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return s.length() == 4;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import java.util.Map;
|
|||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -302,117 +301,4 @@ class ParamUtilsTest {
|
|||
assertDoesNotThrow(() -> ParamUtils.checkParam("dataId", "group", ""));
|
||||
assertDoesNotThrow(() -> ParamUtils.checkParam("dataId", "group", UUID.randomUUID().toString()));
|
||||
}
|
||||
|
||||
// Does not encode when name is already valid
|
||||
@Test
|
||||
void testUsageDoesNotEncodeValidNames() {
|
||||
String input = "abc123";
|
||||
assertTrue(ParamUtils.isValid(input));
|
||||
String processed = input;
|
||||
if (!ParamUtils.isValid(processed)) {
|
||||
processed = ParamUtils.encodeName(processed);
|
||||
}
|
||||
assertEquals(input, processed);
|
||||
}
|
||||
|
||||
// Round-trip encode/decode when input contains a space
|
||||
@Test
|
||||
void testEncodeAndDecodeWhenNameContainsSpace() {
|
||||
String input = "hello world";
|
||||
assertFalse(ParamUtils.isValid(input));
|
||||
String encoded = ParamUtils.encodeName(input);
|
||||
assertTrue(ParamUtils.isEncoded(encoded));
|
||||
assertTrue(encoded.contains("_0020"));
|
||||
assertEquals(input, ParamUtils.decodeName(encoded));
|
||||
}
|
||||
|
||||
// Valid special characters (._:-) should be preserved without encoding
|
||||
@Test
|
||||
void testValidSpecialCharsAreKept() {
|
||||
String input = "name_ok.1:2";
|
||||
assertTrue(ParamUtils.isValid(input));
|
||||
String processed = input;
|
||||
if (!ParamUtils.isValid(processed)) {
|
||||
processed = ParamUtils.encodeName(processed);
|
||||
}
|
||||
assertEquals(input, processed);
|
||||
}
|
||||
|
||||
// Round-trip encode/decode for mixed unicode letters and ASCII
|
||||
@Test
|
||||
void testRoundTripUnicodeChars() {
|
||||
String input = " Ω test";
|
||||
assertFalse(ParamUtils.isValid(input));
|
||||
String encoded = ParamUtils.encodeName(input);
|
||||
String decoded = ParamUtils.decodeName(encoded);
|
||||
assertEquals(input, decoded);
|
||||
}
|
||||
|
||||
// Input starts with underscore followed by hex-like sequence; verify behavior policy
|
||||
@Test
|
||||
void testUnderscoreFollowedByHexAmbiguityHandledByPolicy() {
|
||||
String original = "1 _abcd";
|
||||
if (!ParamUtils.isValid(original)) {
|
||||
String processed = ParamUtils.encodeName(original);
|
||||
assertEquals(original, ParamUtils.decodeName(processed));
|
||||
}
|
||||
}
|
||||
|
||||
// Round-trip for extreme boundary code points (NUL and U+FFFF)
|
||||
@Test
|
||||
void testBoundaryCharacters() {
|
||||
String input = "\u0000\uFFFF";
|
||||
String encoded = ParamUtils.encodeName(input);
|
||||
String decoded = ParamUtils.decodeName(encoded);
|
||||
assertEquals(input, decoded);
|
||||
}
|
||||
|
||||
// Encoding keeps empty string as-is and preserves a single underscore
|
||||
@Test
|
||||
void testEncodeKeepsEmptyAndUnderscore() {
|
||||
String empty = "";
|
||||
String encodedEmpty = ParamUtils.encodeName(empty);
|
||||
assertEquals("", encodedEmpty);
|
||||
assertFalse(ParamUtils.isEncoded(encodedEmpty));
|
||||
|
||||
String underscoreOnly = "_";
|
||||
assertTrue(ParamUtils.isValid(underscoreOnly));
|
||||
String encodedUnderscore = ParamUtils.encodeName(underscoreOnly);
|
||||
assertEquals(underscoreOnly, encodedUnderscore);
|
||||
assertFalse(ParamUtils.isEncoded(encodedUnderscore));
|
||||
}
|
||||
|
||||
// Encoding is idempotent for already-encoded output; decode restores original
|
||||
@Test
|
||||
void testAlreadyEncodedStringIsIdempotentOnEncode() {
|
||||
String original = "with space and Ω and tab\t";
|
||||
String first = ParamUtils.encodeName(original);
|
||||
assertTrue(ParamUtils.isEncoded(first));
|
||||
String second = ParamUtils.encodeName(first);
|
||||
// encodeName should not double-encode an already valid string
|
||||
assertEquals(first, second);
|
||||
// decode should restore original
|
||||
assertEquals(original, ParamUtils.decodeName(first));
|
||||
}
|
||||
|
||||
// Round-trip for mixture of ASCII, control (tab), and underscore suffix
|
||||
@Test
|
||||
void testMixedUnicodeAndControlCharactersRoundTrip() {
|
||||
String original = "A B\tC_";
|
||||
String encoded = ParamUtils.encodeName(original);
|
||||
assertTrue(ParamUtils.isEncoded(encoded));
|
||||
String decoded = ParamUtils.decodeName(encoded);
|
||||
assertEquals(original, decoded);
|
||||
}
|
||||
|
||||
// Decoding a string with encoded prefix returns body; encoding leaves valid input unchanged
|
||||
@Test
|
||||
void testDecodeNameWithFakeEncodedPrefixBody() {
|
||||
String fake = "_-.SYSENC:hello";
|
||||
// This string is already valid; encodeName should return as-is
|
||||
assertTrue(ParamUtils.isValid(fake));
|
||||
assertEquals(fake, ParamUtils.encodeName(fake));
|
||||
// decodeName should strip prefix and return body unchanged
|
||||
assertEquals("hello", ParamUtils.decodeName(fake));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ class AgentDetail extends React.Component {
|
|||
this.setState({ loading: true });
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', agentName);
|
||||
params.append('agentName', agentName);
|
||||
params.append('namespaceId', namespaceId);
|
||||
|
||||
request({
|
||||
|
@ -95,7 +95,7 @@ class AgentDetail extends React.Component {
|
|||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', agentName);
|
||||
params.append('agentName', agentName);
|
||||
params.append('namespaceId', namespaceId);
|
||||
|
||||
request({
|
||||
|
|
|
@ -128,7 +128,7 @@ class AgentManagement extends React.Component {
|
|||
const data = {
|
||||
pageNo: pageNo,
|
||||
pageSize: pageSize,
|
||||
name: searchName || '',
|
||||
agentName: searchName || '',
|
||||
search: 'blur',
|
||||
namespaceId: namespaceId,
|
||||
};
|
||||
|
@ -217,7 +217,7 @@ class AgentManagement extends React.Component {
|
|||
const { locale = {} } = this.props;
|
||||
const namespaceId = getParams('namespace') || '';
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', record.name);
|
||||
params.append('agentName', record.name);
|
||||
if (namespaceId) {
|
||||
params.append('namespaceId', namespaceId);
|
||||
}
|
||||
|
@ -281,7 +281,7 @@ class AgentManagement extends React.Component {
|
|||
|
||||
const deletePromises = selectedRows.map(row => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', row.name);
|
||||
params.append('agentName', row.name);
|
||||
if (namespaceId) {
|
||||
params.append('namespaceId', namespaceId);
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ class NewAgent extends React.Component {
|
|||
this.setState({ loading: true });
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', agentName);
|
||||
params.append('agentName', agentName);
|
||||
params.append('namespaceId', namespaceId);
|
||||
|
||||
request({
|
||||
|
@ -236,7 +236,7 @@ class NewAgent extends React.Component {
|
|||
// 准备请求数据
|
||||
const requestData = {
|
||||
namespaceId: namespaceId,
|
||||
name: values.name,
|
||||
agentName: values.name,
|
||||
version: values.version,
|
||||
registrationType: isEdit ? '' : 'URL', // 默认使用 url 类型
|
||||
agentCard: JSON.stringify(agentCard),
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
|
|||
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
|
||||
import com.alibaba.nacos.ai.param.AgentHttpParamExtractor;
|
||||
import com.alibaba.nacos.ai.utils.AgentRequestUtil;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
|
@ -34,6 +35,7 @@ import com.alibaba.nacos.api.model.v2.Result;
|
|||
import com.alibaba.nacos.auth.annotation.Secured;
|
||||
import com.alibaba.nacos.console.proxy.ai.A2aProxy;
|
||||
import com.alibaba.nacos.core.model.form.PageForm;
|
||||
import com.alibaba.nacos.core.paramcheck.ExtractorManager;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
|
||||
import com.alibaba.nacos.plugin.auth.constant.ApiType;
|
||||
import com.alibaba.nacos.plugin.auth.constant.SignType;
|
||||
|
@ -54,6 +56,7 @@ import java.util.List;
|
|||
@NacosApi
|
||||
@RestController
|
||||
@RequestMapping(Constants.A2A.CONSOLE_PATH)
|
||||
@ExtractorManager.Extractor(httpExtractor = AgentHttpParamExtractor.class)
|
||||
public class ConsoleA2aController {
|
||||
|
||||
private final A2aProxy a2aProxy;
|
||||
|
@ -151,7 +154,7 @@ public class ConsoleA2aController {
|
|||
@Secured(action = ActionTypes.READ, signType = SignType.AI, apiType = ApiType.ADMIN_API)
|
||||
public Result<List<AgentVersionDetail>> listAgentVersions(AgentForm agentForm) throws NacosException {
|
||||
agentForm.validate();
|
||||
return Result.success(a2aProxy.listAgentVersions(agentForm.getNamespaceId(), agentForm.getName()));
|
||||
return Result.success(a2aProxy.listAgentVersions(agentForm.getNamespaceId(), agentForm.getAgentName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.alibaba.nacos.ai.form.a2a.admin.AgentCardForm;
|
|||
import com.alibaba.nacos.ai.form.a2a.admin.AgentCardUpdateForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentForm;
|
||||
import com.alibaba.nacos.ai.form.a2a.admin.AgentListForm;
|
||||
import com.alibaba.nacos.ai.service.A2aServerOperationService;
|
||||
import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo;
|
||||
import com.alibaba.nacos.api.ai.model.a2a.AgentCardVersionInfo;
|
||||
|
@ -61,12 +61,13 @@ public class A2aInnerHandler implements A2aHandler {
|
|||
|
||||
@Override
|
||||
public AgentCardDetailInfo getAgentCardWithVersions(AgentForm form) throws NacosException {
|
||||
return a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getName(), form.getVersion());
|
||||
return a2aServerOperationService.getAgentCard(form.getNamespaceId(), form.getAgentName(), form.getVersion(),
|
||||
form.getRegistrationType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAgent(AgentForm form) throws NacosException {
|
||||
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getName(), form.getVersion());
|
||||
a2aServerOperationService.deleteAgent(form.getNamespaceId(), form.getAgentName(), form.getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -77,7 +78,7 @@ public class A2aInnerHandler implements A2aHandler {
|
|||
|
||||
@Override
|
||||
public Page<AgentCardVersionInfo> listAgents(AgentListForm agentListForm, PageForm pageForm) throws NacosException {
|
||||
return a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getName(),
|
||||
return a2aServerOperationService.listAgents(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
|
||||
agentListForm.getSearch(), pageForm.getPageNo(), pageForm.getPageSize());
|
||||
}
|
||||
|
||||
|
|
|
@ -64,12 +64,12 @@ public class A2aRemoteHandler implements A2aHandler {
|
|||
@Override
|
||||
public AgentCardDetailInfo getAgentCardWithVersions(AgentForm form) throws NacosException {
|
||||
return clientHolder.getAiMaintainerService()
|
||||
.getAgentCard(form.getName(), form.getNamespaceId(), form.getRegistrationType());
|
||||
.getAgentCard(form.getAgentName(), form.getNamespaceId(), form.getRegistrationType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAgent(AgentForm form) throws NacosException {
|
||||
clientHolder.getAiMaintainerService().deleteAgent(form.getName(), form.getNamespaceId());
|
||||
clientHolder.getAiMaintainerService().deleteAgent(form.getAgentName(), form.getNamespaceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -82,9 +82,9 @@ public class A2aRemoteHandler implements A2aHandler {
|
|||
public Page<AgentCardVersionInfo> listAgents(AgentListForm agentListForm, PageForm pageForm) throws NacosException {
|
||||
AiMaintainerService aiMaintainerService = clientHolder.getAiMaintainerService();
|
||||
return Constants.MCP_LIST_SEARCH_BLUR.equalsIgnoreCase(agentListForm.getSearch())
|
||||
? aiMaintainerService.searchAgentCardsByName(agentListForm.getNamespaceId(), agentListForm.getName(),
|
||||
? aiMaintainerService.searchAgentCardsByName(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
|
||||
pageForm.getPageNo(), pageForm.getPageSize())
|
||||
: aiMaintainerService.listAgentCards(agentListForm.getNamespaceId(), agentListForm.getName(),
|
||||
: aiMaintainerService.listAgentCards(agentListForm.getNamespaceId(), agentListForm.getAgentName(),
|
||||
pageForm.getPageNo(), pageForm.getPageSize());
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<link rel="stylesheet" type="text/css" href="console-ui/public/css/icon.css">
|
||||
<link rel="stylesheet" type="text/css" href="console-ui/public/css/font-awesome.css">
|
||||
<!-- 第三方css结束 -->
|
||||
<link href="./css/main.css?a5493de22114f2937036" rel="stylesheet"></head>
|
||||
<link href="./css/main.css?ff94db2b949ca2c50f35" rel="stylesheet"></head>
|
||||
|
||||
<body>
|
||||
<div id="root" style="overflow:hidden"></div>
|
||||
|
@ -56,6 +56,6 @@
|
|||
<script src="console-ui/public/js/merge.js"></script>
|
||||
<script src="console-ui/public/js/loader.js"></script>
|
||||
<!-- 第三方js结束 -->
|
||||
<script type="text/javascript" src="./js/main.js?a5493de22114f2937036"></script></body>
|
||||
<script type="text/javascript" src="./js/main.js?ff94db2b949ca2c50f35"></script></body>
|
||||
|
||||
</html>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 1999-2025 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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 com.alibaba.nacos.core.paramcheck.impl;
|
||||
|
||||
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
|
||||
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
|
||||
import com.alibaba.nacos.api.exception.NacosException;
|
||||
import com.alibaba.nacos.api.remote.request.Request;
|
||||
import com.alibaba.nacos.common.paramcheck.ParamInfo;
|
||||
import com.alibaba.nacos.core.paramcheck.AbstractRpcParamExtractor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Nacos A2A(Agent & Agent Card) grpc request param extractor.
|
||||
*
|
||||
* @author xiweng.yy
|
||||
*/
|
||||
public class AgentRequestParamExtractor extends AbstractRpcParamExtractor {
|
||||
|
||||
@Override
|
||||
public List<ParamInfo> extractParam(Request request) throws NacosException {
|
||||
AbstractAgentRequest agentRequest = (AbstractAgentRequest) request;
|
||||
ParamInfo paramInfo = new ParamInfo();
|
||||
paramInfo.setNamespaceId(agentRequest.getNamespaceId());
|
||||
paramInfo.setAgentName(agentRequest.getAgentName());
|
||||
if (agentRequest instanceof ReleaseAgentCardRequest) {
|
||||
ReleaseAgentCardRequest releaseAgentCardRequest = (ReleaseAgentCardRequest) agentRequest;
|
||||
if (null != releaseAgentCardRequest.getAgentCard()) {
|
||||
paramInfo.setAgentName(releaseAgentCardRequest.getAgentCard().getName());
|
||||
}
|
||||
}
|
||||
return List.of(paramInfo);
|
||||
}
|
||||
}
|
|
@ -22,4 +22,5 @@ com.alibaba.nacos.core.paramcheck.impl.PersistentInstanceRequestParamExtractor
|
|||
com.alibaba.nacos.core.paramcheck.impl.ConfigRequestParamExtractor
|
||||
com.alibaba.nacos.core.paramcheck.impl.ConfigBatchListenRequestParamExtractor
|
||||
com.alibaba.nacos.core.paramcheck.impl.BatchInstanceRequestParamExtractor
|
||||
com.alibaba.nacos.core.paramcheck.impl.McpServerRequestParamExtractor
|
||||
com.alibaba.nacos.core.paramcheck.impl.McpServerRequestParamExtractor
|
||||
com.alibaba.nacos.core.paramcheck.impl.AgentRequestParamExtractor
|
|
@ -215,7 +215,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
Map<String, String> params = new HashMap<>(4);
|
||||
params.put("agentCard", JacksonUtils.toJson(agentCard));
|
||||
params.put("namespaceId", namespaceId);
|
||||
params.put("name", agentCard.getName());
|
||||
params.put("agentName", agentCard.getName());
|
||||
params.put("registrationType", registrationType);
|
||||
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.POST).setParamValue(params)
|
||||
.setPath(Constants.AdminApiPath.AI_AGENT_ADMIN_PATH).build();
|
||||
|
@ -232,7 +232,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
RequestResource resource = buildRequestResource(namespaceId, agentName);
|
||||
|
||||
Map<String, String> params = new HashMap<>(1);
|
||||
params.put("name", agentName);
|
||||
params.put("agentName", agentName);
|
||||
params.put("namespaceId", namespaceId);
|
||||
params.put("registrationType", registrationType);
|
||||
|
||||
|
@ -252,7 +252,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
Map<String, String> params = new HashMap<>(5);
|
||||
params.put("agentCard", JacksonUtils.toJson(agentCard));
|
||||
params.put("namespaceId", namespaceId);
|
||||
params.put("name", agentCard.getName());
|
||||
params.put("agentName", agentCard.getName());
|
||||
params.put("setAsLatest", String.valueOf(setAsLatest));
|
||||
params.put("registrationType", registrationType);
|
||||
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.PUT).setParamValue(params)
|
||||
|
@ -269,7 +269,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
RequestResource resource = buildRequestResource(namespaceId, agentName);
|
||||
|
||||
Map<String, String> params = new HashMap<>(1);
|
||||
params.put("name", agentName);
|
||||
params.put("agentName", agentName);
|
||||
params.put("namespaceId", namespaceId);
|
||||
params.put("version", version);
|
||||
|
||||
|
@ -287,7 +287,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
RequestResource resource = buildRequestResource(namespaceId, agentName);
|
||||
|
||||
Map<String, String> params = new HashMap<>(1);
|
||||
params.put("name", agentName);
|
||||
params.put("agentName", agentName);
|
||||
params.put("namespaceId", namespaceId);
|
||||
|
||||
HttpRequest request = buildHttpRequestBuilder(resource).setHttpMethod(HttpMethod.GET).setParamValue(params)
|
||||
|
@ -315,7 +315,7 @@ public class NacosAiMaintainerServiceImpl implements AiMaintainerService {
|
|||
int pageSize, boolean isBlur) throws NacosException {
|
||||
RequestResource resource = buildRequestResource(namespaceId, null);
|
||||
Map<String, String> params = new HashMap<>(1);
|
||||
params.put("name", agentName);
|
||||
params.put("agentName", agentName);
|
||||
params.put("namespaceId", namespaceId);
|
||||
params.put("search", isBlur ? SEARCH_BLUR : SEARCH_ACCURATE);
|
||||
params.put("pageNo", String.valueOf(pageNo));
|
||||
|
|
Loading…
Reference in New Issue