Skip to content
Open
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 @@ -31,6 +31,7 @@
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.ToolResponseMessage.ToolResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
Expand Down Expand Up @@ -124,7 +125,8 @@ public Prompt toLlmPrompt(LlmRequest llmRequest) {
List<ToolCallback> toolCallbacks = toolConverter.convertToSpringAiTools(llmRequest.tools());
if (!toolCallbacks.isEmpty()) {
// Create new ChatOptions with tools included
ToolCallingChatOptions.Builder optionsBuilder = ToolCallingChatOptions.builder();
ToolCallingChatOptions.Builder optionsBuilder =
ToolCallingChatOptions.builder().internalToolExecutionEnabled(false);

// Always set tool callbacks
optionsBuilder.toolCallbacks(toolCallbacks);
Expand Down Expand Up @@ -204,10 +206,26 @@ private List<Message> handleUserContent(Content content) {
if (part.text().isPresent()) {
textBuilder.append(part.text().get());
} else if (part.functionResponse().isPresent()) {
// TODO: Spring AI 1.1.0 ToolResponseMessage constructors are protected
// For now, we skip tool responses in user messages
// This will need to be addressed in a future update when Spring AI provides
// a public API for creating ToolResponseMessage
var functionResponse = part.functionResponse().get();
functionResponse
.id()
.ifPresent(
id ->
functionResponse
.name()
.ifPresent(
name ->
functionResponse
.response()
.ifPresent(
response ->
toolResponseMessages.add(
ToolResponseMessage.builder()
.responses(
List.of(
new ToolResponse(
id, name, toJson(response))))
.build()))));
} else if (part.inlineData().isPresent()) {
// Handle inline media data (images, audio, video, etc.)
com.google.genai.types.Blob blob = part.inlineData().get();
Expand Down Expand Up @@ -243,12 +261,11 @@ private List<Message> handleUserContent(Content content) {
}

List<Message> messages = new ArrayList<>();
// Create UserMessage with text
// TODO: Media attachments support - UserMessage constructors with media are private in Spring
// AI 1.1.0
// For now, only text content is supported
messages.add(new UserMessage(textBuilder.toString()));
messages.addAll(toolResponseMessages);
if (!toolResponseMessages.isEmpty()) {
messages.addAll(toolResponseMessages);
} else {
messages.add(UserMessage.builder().text(textBuilder.toString()).media(mediaList).build());
}

return messages;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,34 +120,8 @@ public List<ToolCallback> convertToSpringAiTools(Map<String, BaseTool> tools) {
FunctionDeclaration declaration = tool.declaration().get();

// Create a ToolCallback that wraps the ADK tool
// Create a Function that takes Map input and calls the ADK tool
java.util.function.Function<Map<String, Object>, String> toolFunction =
args -> {
try {
logger.debug("Spring AI calling tool '{}'", tool.name());
logger.debug("Raw args from Spring AI: {}", args);
logger.debug("Args type: {}", args.getClass().getName());
logger.debug("Args keys: {}", args.keySet());
for (Map.Entry<String, Object> entry : args.entrySet()) {
logger.debug(
" {} -> {} ({})",
entry.getKey(),
entry.getValue(),
entry.getValue().getClass().getName());
}

// Handle different argument formats that Spring AI might pass
Map<String, Object> processedArgs = processArguments(args, declaration);
logger.debug("Processed args for ADK: {}", processedArgs);

// Call the ADK tool and wait for the result
Map<String, Object> result = tool.runAsync(processedArgs, null).blockingGet();
// Convert result back to JSON string
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(result);
} catch (Exception e) {
throw new RuntimeException("Tool execution failed: " + e.getMessage(), e);
}
};
// Create a Function that does nothing. Function calling is done by ADK.
java.util.function.Function<Map<String, Object>, String> toolFunction = args -> "";

FunctionToolCallback.Builder callbackBuilder =
FunctionToolCallback.builder(tool.name(), toolFunction).description(tool.description());
Expand Down Expand Up @@ -181,54 +155,6 @@ public List<ToolCallback> convertToSpringAiTools(Map<String, BaseTool> tools) {
return toolCallbacks;
}

/**
* Process arguments from Spring AI format to ADK format. Spring AI might pass arguments in
* different formats depending on the provider.
*/
private Map<String, Object> processArguments(
Map<String, Object> args, FunctionDeclaration declaration) {
// If the arguments already match the expected format, return as-is
if (declaration.parameters().isPresent()) {
var schema = declaration.parameters().get();
if (schema.properties().isPresent()) {
var expectedParams = schema.properties().get().keySet();

// Check if all expected parameters are present at the top level
boolean allParamsPresent = expectedParams.stream().allMatch(args::containsKey);
if (allParamsPresent) {
return args;
}

// Check if arguments are nested under a single key (common pattern)
if (args.size() == 1) {
var singleValue = args.values().iterator().next();
if (singleValue instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nestedArgs = (Map<String, Object>) singleValue;
boolean allNestedParamsPresent =
expectedParams.stream().allMatch(nestedArgs::containsKey);
if (allNestedParamsPresent) {
return nestedArgs;
}
}
}

// Check if we have a single parameter function and got a direct value
if (expectedParams.size() == 1) {
String expectedParam = expectedParams.iterator().next();
if (args.size() == 1 && !args.containsKey(expectedParam)) {
// Try to map the single value to the expected parameter name
Object singleValue = args.values().iterator().next();
return Map.of(expectedParam, singleValue);
}
}
}
}

// If no processing worked, return original args and let ADK handle the error
return args;
}

/** Simple metadata holder for tool information. */
public static class ToolMetadata {
private final String name;
Expand Down

This file was deleted.