Skip to content

Commit 132fc40

Browse files
committed
feat(a2a): Add agent search endpoint with capability filtering (#6996)
Implements Phase 2 of A2A Agent Card support: Capability Search. This enables discovery of agents based on their capabilities and skills. Changes: - Add GET /.well-known/agents endpoint for searching agent cards - Support filtering by skill, capability, input mode, output mode - Create AgentCardLabelExtractor to extract searchable labels from agent cards - Create AgentCardLabelsStorageDecorator to auto-index agent cards on creation - Add AgentSearchResults and AgentSearchResult DTOs - Add tests for agent search functionality Part of #6996
1 parent 2efd3d9 commit 132fc40

File tree

8 files changed

+661
-1
lines changed

8 files changed

+661
-1
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.apicurio.registry.a2a;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.apicurio.registry.content.ContentHandle;
6+
import jakarta.enterprise.context.ApplicationScoped;
7+
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
/**
12+
* Extracts searchable labels from Agent Card content.
13+
* These labels are stored with the artifact and enable capability-based search.
14+
*/
15+
@ApplicationScoped
16+
public class AgentCardLabelExtractor {
17+
18+
public static final String LABEL_SKILL_PREFIX = "a2a.skill.";
19+
public static final String LABEL_CAPABILITY_PREFIX = "a2a.capability.";
20+
public static final String LABEL_INPUT_MODE_PREFIX = "a2a.inputMode.";
21+
public static final String LABEL_OUTPUT_MODE_PREFIX = "a2a.outputMode.";
22+
public static final String LABEL_AGENT_NAME = "a2a.name";
23+
public static final String LABEL_AGENT_URL = "a2a.url";
24+
25+
private static final ObjectMapper mapper = new ObjectMapper();
26+
27+
/**
28+
* Extracts labels from agent card JSON content.
29+
*
30+
* @param content the agent card JSON content
31+
* @return a map of labels to be stored with the artifact
32+
*/
33+
public Map<String, String> extractLabels(ContentHandle content) {
34+
Map<String, String> labels = new HashMap<>();
35+
36+
try {
37+
JsonNode root = mapper.readTree(content.content());
38+
39+
// Extract name
40+
if (root.has("name") && root.get("name").isTextual()) {
41+
labels.put(LABEL_AGENT_NAME, root.get("name").asText());
42+
}
43+
44+
// Extract URL
45+
if (root.has("url") && root.get("url").isTextual()) {
46+
labels.put(LABEL_AGENT_URL, root.get("url").asText());
47+
}
48+
49+
// Extract capabilities
50+
if (root.has("capabilities") && root.get("capabilities").isObject()) {
51+
JsonNode capabilities = root.get("capabilities");
52+
53+
if (capabilities.has("streaming")) {
54+
labels.put(LABEL_CAPABILITY_PREFIX + "streaming",
55+
String.valueOf(capabilities.get("streaming").asBoolean(false)));
56+
}
57+
if (capabilities.has("pushNotifications")) {
58+
labels.put(LABEL_CAPABILITY_PREFIX + "pushNotifications",
59+
String.valueOf(capabilities.get("pushNotifications").asBoolean(false)));
60+
}
61+
}
62+
63+
// Extract skills
64+
if (root.has("skills") && root.get("skills").isArray()) {
65+
for (JsonNode skill : root.get("skills")) {
66+
if (skill.has("id") && skill.get("id").isTextual()) {
67+
String skillId = skill.get("id").asText();
68+
String skillName = skill.has("name") ? skill.get("name").asText() : skillId;
69+
labels.put(LABEL_SKILL_PREFIX + skillId, skillName);
70+
}
71+
}
72+
}
73+
74+
// Extract default input modes
75+
if (root.has("defaultInputModes") && root.get("defaultInputModes").isArray()) {
76+
for (JsonNode mode : root.get("defaultInputModes")) {
77+
if (mode.isTextual()) {
78+
labels.put(LABEL_INPUT_MODE_PREFIX + mode.asText(), "true");
79+
}
80+
}
81+
}
82+
83+
// Extract default output modes
84+
if (root.has("defaultOutputModes") && root.get("defaultOutputModes").isArray()) {
85+
for (JsonNode mode : root.get("defaultOutputModes")) {
86+
if (mode.isTextual()) {
87+
labels.put(LABEL_OUTPUT_MODE_PREFIX + mode.asText(), "true");
88+
}
89+
}
90+
}
91+
92+
} catch (Exception e) {
93+
// If parsing fails, return empty labels - validation will catch invalid content
94+
}
95+
96+
return labels;
97+
}
98+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package io.apicurio.registry.a2a.rest.beans;
2+
3+
import java.util.List;
4+
5+
/**
6+
* A single agent card search result with metadata.
7+
*/
8+
public class AgentSearchResult {
9+
10+
private String groupId;
11+
private String artifactId;
12+
private String name;
13+
private String description;
14+
private String version;
15+
private String url;
16+
private List<String> skills;
17+
private AgentCapabilities capabilities;
18+
private long createdOn;
19+
private String owner;
20+
21+
public AgentSearchResult() {
22+
}
23+
24+
public String getGroupId() {
25+
return groupId;
26+
}
27+
28+
public void setGroupId(String groupId) {
29+
this.groupId = groupId;
30+
}
31+
32+
public String getArtifactId() {
33+
return artifactId;
34+
}
35+
36+
public void setArtifactId(String artifactId) {
37+
this.artifactId = artifactId;
38+
}
39+
40+
public String getName() {
41+
return name;
42+
}
43+
44+
public void setName(String name) {
45+
this.name = name;
46+
}
47+
48+
public String getDescription() {
49+
return description;
50+
}
51+
52+
public void setDescription(String description) {
53+
this.description = description;
54+
}
55+
56+
public String getVersion() {
57+
return version;
58+
}
59+
60+
public void setVersion(String version) {
61+
this.version = version;
62+
}
63+
64+
public String getUrl() {
65+
return url;
66+
}
67+
68+
public void setUrl(String url) {
69+
this.url = url;
70+
}
71+
72+
public List<String> getSkills() {
73+
return skills;
74+
}
75+
76+
public void setSkills(List<String> skills) {
77+
this.skills = skills;
78+
}
79+
80+
public AgentCapabilities getCapabilities() {
81+
return capabilities;
82+
}
83+
84+
public void setCapabilities(AgentCapabilities capabilities) {
85+
this.capabilities = capabilities;
86+
}
87+
88+
public long getCreatedOn() {
89+
return createdOn;
90+
}
91+
92+
public void setCreatedOn(long createdOn) {
93+
this.createdOn = createdOn;
94+
}
95+
96+
public String getOwner() {
97+
return owner;
98+
}
99+
100+
public void setOwner(String owner) {
101+
this.owner = owner;
102+
}
103+
104+
public static Builder builder() {
105+
return new Builder();
106+
}
107+
108+
public static class Builder {
109+
private final AgentSearchResult result = new AgentSearchResult();
110+
111+
public Builder groupId(String groupId) {
112+
result.groupId = groupId;
113+
return this;
114+
}
115+
116+
public Builder artifactId(String artifactId) {
117+
result.artifactId = artifactId;
118+
return this;
119+
}
120+
121+
public Builder name(String name) {
122+
result.name = name;
123+
return this;
124+
}
125+
126+
public Builder description(String description) {
127+
result.description = description;
128+
return this;
129+
}
130+
131+
public Builder version(String version) {
132+
result.version = version;
133+
return this;
134+
}
135+
136+
public Builder url(String url) {
137+
result.url = url;
138+
return this;
139+
}
140+
141+
public Builder skills(List<String> skills) {
142+
result.skills = skills;
143+
return this;
144+
}
145+
146+
public Builder capabilities(AgentCapabilities capabilities) {
147+
result.capabilities = capabilities;
148+
return this;
149+
}
150+
151+
public Builder createdOn(long createdOn) {
152+
result.createdOn = createdOn;
153+
return this;
154+
}
155+
156+
public Builder owner(String owner) {
157+
result.owner = owner;
158+
return this;
159+
}
160+
161+
public AgentSearchResult build() {
162+
return result;
163+
}
164+
}
165+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.apicurio.registry.a2a.rest.beans;
2+
3+
import java.util.List;
4+
5+
/**
6+
* Search results for agent cards.
7+
*/
8+
public class AgentSearchResults {
9+
10+
private long count;
11+
private List<AgentSearchResult> agents;
12+
13+
public AgentSearchResults() {
14+
}
15+
16+
public AgentSearchResults(long count, List<AgentSearchResult> agents) {
17+
this.count = count;
18+
this.agents = agents;
19+
}
20+
21+
public long getCount() {
22+
return count;
23+
}
24+
25+
public void setCount(long count) {
26+
this.count = count;
27+
}
28+
29+
public List<AgentSearchResult> getAgents() {
30+
return agents;
31+
}
32+
33+
public void setAgents(List<AgentSearchResult> agents) {
34+
this.agents = agents;
35+
}
36+
37+
public static Builder builder() {
38+
return new Builder();
39+
}
40+
41+
public static class Builder {
42+
private long count;
43+
private List<AgentSearchResult> agents;
44+
45+
public Builder count(long count) {
46+
this.count = count;
47+
return this;
48+
}
49+
50+
public Builder agents(List<AgentSearchResult> agents) {
51+
this.agents = agents;
52+
return this;
53+
}
54+
55+
public AgentSearchResults build() {
56+
return new AgentSearchResults(count, agents);
57+
}
58+
}
59+
}

app/src/main/java/io/apicurio/registry/rest/wellknown/WellKnownResource.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.apicurio.registry.rest.wellknown;
22

33
import io.apicurio.registry.a2a.rest.beans.AgentCard;
4+
import io.apicurio.registry.a2a.rest.beans.AgentSearchResults;
5+
import jakarta.ws.rs.DefaultValue;
46
import jakarta.ws.rs.GET;
57
import jakarta.ws.rs.Path;
68
import jakarta.ws.rs.PathParam;
@@ -9,6 +11,8 @@
911
import jakarta.ws.rs.core.MediaType;
1012
import jakarta.ws.rs.core.Response;
1113

14+
import java.util.List;
15+
1216
/**
1317
* JAX-RS resource for A2A protocol well-known endpoints.
1418
*
@@ -47,4 +51,29 @@ Response getRegisteredAgentCard(
4751
@PathParam("groupId") String groupId,
4852
@PathParam("artifactId") String artifactId,
4953
@QueryParam("version") String version);
54+
55+
/**
56+
* Search for registered Agent Cards by various criteria.
57+
* This enables discovery of agents based on their capabilities and skills.
58+
*
59+
* @param name filter by agent name (partial match)
60+
* @param skill filter by skill ID (can be specified multiple times)
61+
* @param capability filter by capability (e.g., "streaming:true")
62+
* @param inputMode filter by input mode (e.g., "text", "image")
63+
* @param outputMode filter by output mode
64+
* @param offset pagination offset
65+
* @param limit pagination limit
66+
* @return search results containing matching agent cards
67+
*/
68+
@GET
69+
@Path("/agents")
70+
@Produces(MediaType.APPLICATION_JSON)
71+
AgentSearchResults searchAgents(
72+
@QueryParam("name") String name,
73+
@QueryParam("skill") List<String> skills,
74+
@QueryParam("capability") List<String> capabilities,
75+
@QueryParam("inputMode") List<String> inputModes,
76+
@QueryParam("outputMode") List<String> outputModes,
77+
@QueryParam("offset") @DefaultValue("0") Integer offset,
78+
@QueryParam("limit") @DefaultValue("20") Integer limit);
5079
}

0 commit comments

Comments
 (0)