Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
public class SetModelResponseTool extends BaseTool {
public static final String NAME = "set_model_response";

// Prefix of the SchemaUtils validation message after which the full schema is appended. Used to
// strip the schema dump from feedback on a best-effort basis; if SchemaUtils changes its wording
// the feedback simply stays unstripped. runAsync_unknownArg_feedbackOmitsSchemaDump pins the
// current format.
// Marker after which SchemaUtils appends the schema dump; stripped from feedback best-effort.
private static final String OUTPUT_SCHEMA_DUMP_MARKER = " does not match agent output schema: ";

private final Schema outputSchema;
Expand Down Expand Up @@ -92,21 +89,22 @@ private static Map<String, Object> excludeNullFields(Map<String, Object> values)
for (Map.Entry<String, Object> entry : values.entrySet()) {
Object value = entry.getValue();
if (value != null) {
result.put(entry.getKey(), excludeNullFields(value));
result.put(entry.getKey(), excludeNullsFromValue(value));
}
}
return result;
}

@SuppressWarnings("unchecked")
private static Object excludeNullFields(Object value) {
private static Object excludeNullsFromValue(Object value) {
if (value instanceof Map<?, ?>) {
return excludeNullFields((Map<String, Object>) value);
@SuppressWarnings("unchecked") // Function-call args always have String keys.
Map<String, Object> map = (Map<String, Object>) value;
return excludeNullFields(map);
}
if (value instanceof List<?>) {
List<Object> result = new ArrayList<>();
for (Object item : (List<?>) value) {
result.add(excludeNullFields(item));
result.add(excludeNullsFromValue(item));
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ public void getStructuredModelResponse_withValidationFeedback_returnsEmpty() {
.response(
ImmutableMap.of(
"error",
"Validation Error found: field1 is required. Fix the errors and call it again."))
"Validation Error found: field1 is required. Fix the errors and call it"
+ " again."))
.build();
Event event =
Event.builder()
Expand Down Expand Up @@ -365,19 +366,19 @@ public BaseLlmConnection connect(LlmRequest llmRequest) {
// The retry request must carry the validation feedback from the first call back to the model:
// the feedback names the missing required field, tying it to the first call's failure.
assertThat(scriptedLlm.getRequests()).hasSize(2);
boolean feedbackSentBack =
Optional<String> feedbackError =
scriptedLlm.getRequests().get(1).contents().stream()
.flatMap(content -> content.parts().orElse(ImmutableList.of()).stream())
.map(Part::functionResponse)
.flatMap(Optional::stream)
.anyMatch(
fr ->
Objects.equals(fr.name().orElse(""), SetModelResponseTool.NAME)
&& fr.response().orElse(ImmutableMap.of()).get("error")
instanceof String error
&& error.contains("Validation Error found")
&& error.contains("field1"));
assertThat(feedbackSentBack).isTrue();
.filter(fr -> Objects.equals(fr.name().orElse(""), SetModelResponseTool.NAME))
.map(fr -> fr.response().orElse(ImmutableMap.of()).get("error"))
.filter(String.class::isInstance)
.map(String.class::cast)
.findFirst();
assertThat(feedbackError).isPresent();
assertThat(feedbackError.get()).contains("Validation Error found");
assertThat(feedbackError.get()).contains("field1");

// Only the corrected, validated response becomes the final structured output.
Event finalEvent = events.get(events.size() - 1);
Expand Down
Loading