Skip to content

Commit a9503b2

Browse files
Copilotphrocker
andauthored
Fix chat agent argument parsing and error handling for LLM verb execution (#38)
* Initial plan * Fix chat agent to properly serialize verb execution results Co-authored-by: phrocker <[email protected]> * Fix argument parsing and improve error handling for LLM verb execution Co-authored-by: phrocker <[email protected]> * Address code review feedback: improve field names and add logging Co-authored-by: phrocker <[email protected]> * Fixup --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]> Co-authored-by: Marc Parisi <[email protected]>
1 parent 366d775 commit a9503b2

File tree

2 files changed

+116
-20
lines changed

2 files changed

+116
-20
lines changed

enterprise-agent/src/main/java/io/sentrius/agent/analysis/agents/agents/ChatAgent.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
229229
var responses = agentExecutionContext.getAgentDataList();
230230
var planResponse =
231231
responses.isEmpty() ? "" :
232-
responses.get(responses.size() - 1).asText();
232+
responses.get(responses.size() - 1).toString();
233233
nextResponse = chatVerbs.interpret_plan_response(
234234
agentExecution,
235235
agentExecutionContext,
@@ -276,11 +276,38 @@ public void onApplicationEvent(final ApplicationReadyEvent event) {
276276
}
277277
allowedFailures = 20; // Reset allowed failures on successful heartbeat
278278
} catch (ZtatException | Exception ex) {
279+
// Build a more informative error message for the LLM
280+
StringBuilder errorMsg = new StringBuilder();
281+
errorMsg.append("Error executing operation");
282+
283+
if (response != null && response.getNextOperation() != null) {
284+
errorMsg.append(" '").append(response.getNextOperation()).append("'");
285+
286+
// Add verb signature if available
287+
var verb = verbRegistry.getVerbs().get(response.getNextOperation());
288+
if (verb != null) {
289+
errorMsg.append(".\n\nExpected format for this operation:\n");
290+
errorMsg.append("- Operation name: ").append(verb.getName()).append("\n");
291+
if (verb.getArgName() != null && !verb.getArgName().isEmpty()) {
292+
errorMsg.append("- Argument name: ").append(verb.getArgName()).append("\n");
293+
}
294+
if (verb.getExampleJson() != null && !verb.getExampleJson().isEmpty()) {
295+
errorMsg.append("- Example format: ").append(verb.getExampleJson()).append("\n");
296+
}
297+
errorMsg.append("\nYour arguments were: ").append(
298+
response.getArguments() != null ? response.getArguments().toString() : "null"
299+
);
300+
}
301+
}
302+
303+
errorMsg.append("\n\nError details: ").append(ex.getMessage());
304+
errorMsg.append("\n\nPlease adjust your arguments to match the expected format and try again OR " +
305+
"try a different verb if you don't have the correct arguments at all." +
306+
".");
307+
279308
agentExecutionContext.addMessages(Message.builder().role("system").content(
280-
"You caused the following error. Please re-validate you chose the right operations or " +
281-
"endpoints for the context" +
282-
ex.getMessage()).build());
283-
309+
errorMsg.toString()
310+
).build());
284311

285312
ex.printStackTrace();
286313
if (allowedFailures-- <= 0) {

enterprise-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/AgentVerbs.java

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import io.sentrius.sso.core.dto.agents.AgentContextRequestDTO;
4343
import io.sentrius.sso.core.dto.agents.AgentExecutionContextDTO;
4444
import io.sentrius.sso.core.dto.agents.AgentExecution;
45+
import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor;
4546
import io.sentrius.sso.core.dto.ztat.AtatRequest;
4647
import io.sentrius.sso.core.dto.ztat.TokenDTO;
4748
import io.sentrius.sso.core.dto.ztat.ZtatRequestDTO;
@@ -80,6 +81,10 @@ public class AgentVerbs extends VerbBase {
8081

8182
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); // Jackson ObjectMapper for YAML parsing
8283
private final AgentExecutionService agentExecutionService;
84+
85+
// Field names to search when extracting query strings from nested JSON structures
86+
private static final String[] QUERY_FIELD_NAMES = {"arg1", "field", "context", "query", "text", "value"};
87+
8388

8489
/**
8590
* Constructs an `AgentVerbs` instance with the required services and registry.
@@ -804,8 +809,9 @@ public ObjectNode createAgent(AgentExecution execution, AgentExecutionContextDTO
804809
return contextNode;
805810
}
806811

807-
@Verb(name = "get_agent_status", returnType = AgentExecutionContextDTO.class, description = "Queries the agent status. Can" +
808-
" be Running, pending, NotFound, or Failed" ,
812+
@Verb(name = "get_agent_status", returnType = AgentExecutionContextDTO.class, description = "Queries the agent " +
813+
"status for other agents. Not to be used internally. Can" +
814+
" be Running, pending, NotFound, or Failed." ,
809815
exampleJson = "{ \"agentName\": \"agentName\" }",
810816
requiresTokenManagement = true )
811817
public ObjectNode getAgentStatus(AgentExecution execution, AgentExecutionContextDTO agentIdentifier)
@@ -825,6 +831,8 @@ public ObjectNode getAgentStatus(AgentExecution execution, AgentExecutionContext
825831
return contextNode;
826832
}
827833

834+
835+
828836
@Verb(name = "get_endpoints_like", returnType = AgentExecutionContextDTO.class, description = "Queries for endpoints in " +
829837
"the system that match the input text." ,
830838
returnName = "endpoints",
@@ -841,33 +849,94 @@ public ObjectNode getEndpointsLike(AgentExecution execution,
841849
var parsedQuery = queryInput.get("endpoints_like");
842850

843851
if (null == parsedQuery) {
844-
throw new IllegalArgumentException("Missing 'endpoints_like' argument");
852+
throw new IllegalArgumentException("Missing 'endpoints_like' argument. Expected format: " +
853+
"{ \"endpoints_like\": [\"query text 1\", \"query text 2\"] }");
845854
}
855+
846856
ObjectNode contextNode = JsonUtil.MAPPER.createObjectNode();
847857
ArrayNode endpoints = JsonUtil.MAPPER.createArrayNode();
848-
for(JsonNode node : parsedQuery) {
849-
if (!node.isTextual()) {
850-
if (node.has("arg1")){
851-
node = node.get("arg1");
852-
if (!node.isTextual()) {
853-
throw new IllegalArgumentException("All items in 'endpoints_like' must be strings");
854-
}
858+
859+
// Handle different input formats from the LLM
860+
List<String> queryStrings = new ArrayList<>();
861+
862+
if (parsedQuery.isArray()) {
863+
// Expected format: ["text1", "text2"]
864+
for (JsonNode node : parsedQuery) {
865+
String queryText = extractQueryString(node);
866+
if (queryText != null && !queryText.isEmpty()) {
867+
queryStrings.add(queryText);
855868
}
856869
}
857-
var endpointList = endpointSearcher.getEndpointsLike(execution, node.asText());
858-
JsonNode finalNode = node;
859-
endpointList.forEach(endpoint -> {
870+
} else if (parsedQuery.isObject()) {
871+
// Handle nested object format: {"arg1": {"field": "text"}} or {"arg1": "text"}
872+
String queryText = extractQueryString(parsedQuery);
873+
if (queryText != null && !queryText.isEmpty()) {
874+
queryStrings.add(queryText);
875+
}
876+
} else if (parsedQuery.isTextual()) {
877+
// Simple string format
878+
queryStrings.add(parsedQuery.asText());
879+
}
880+
881+
if (queryStrings.isEmpty()) {
882+
throw new IllegalArgumentException("Could not extract any valid query strings from 'endpoints_like'. " +
883+
"Expected format: { \"endpoints_like\": [\"query text 1\", \"query text 2\"] }. " +
884+
"Received: " + parsedQuery.toString());
885+
}
886+
887+
// Query endpoints for each search string
888+
for (String queryText : queryStrings) {
889+
log.info("Searching endpoints for: {}", queryText);
890+
var endpointList = endpointSearcher.getEndpointsLike(execution, queryText);
891+
for (EndpointDescriptor endpoint : endpointList) {
860892
ObjectNode endpointNode = JsonUtil.MAPPER.createObjectNode();
861-
endpointNode.put("name", finalNode.asText());
893+
endpointNode.put("name", endpoint.getName());
894+
endpointNode.put("description", endpoint.getDescription());
862895
endpointNode.put("method", endpoint.getHttpMethod());
863896
endpointNode.put("endpoint", endpoint.getPath());
897+
endpointNode.put("searchQuery", queryText);
864898
endpoints.add(endpointNode);
865-
});
899+
}
866900
}
901+
867902
contextNode.put("endpoints", endpoints);
868903

869904
return contextNode;
870905
}
906+
907+
/**
908+
* Recursively extracts a query string from a JsonNode, handling various nested structures.
909+
* Tries common patterns like {"arg1": "text"}, {"field": "text"}, {"context": "text"}, etc.
910+
*/
911+
private String extractQueryString(JsonNode node) {
912+
if (node == null) {
913+
return null;
914+
}
915+
916+
if (node.isTextual()) {
917+
return node.asText();
918+
}
919+
920+
if (node.isObject()) {
921+
// Try common field names that might contain the query
922+
for (String fieldName : QUERY_FIELD_NAMES) {
923+
if (node.has(fieldName)) {
924+
log.debug("Extracting query string from field: {}", fieldName);
925+
return extractQueryString(node.get(fieldName));
926+
}
927+
}
928+
929+
// If no known fields, try the first field as fallback
930+
var fields = node.fields();
931+
if (fields.hasNext()) {
932+
var entry = fields.next();
933+
log.warn("No recognized query field found in object, using first field: {}", entry.getKey());
934+
return extractQueryString(entry.getValue());
935+
}
936+
}
937+
938+
return null;
939+
}
871940

872941
@Verb(name = "call_endpoint", returnType = AgentExecutionContextDTO.class, description = "Executes an endpoint at the " +
873942
"service. Input ", exampleJson = "{ \"endpoint\": \"<url>\", \"method\": \"httpMethod\", \"params\": { " +

0 commit comments

Comments
 (0)