Skip to content

fix: convert functionResponse parts to Spring AI ToolResponseMessage - #1424

Closed
hemasekhar-p wants to merge 1 commit into
google:mainfrom
hemasekhar-p:springai-tool-calling-fix
Closed

fix: convert functionResponse parts to Spring AI ToolResponseMessage#1424
hemasekhar-p wants to merge 1 commit into
google:mainfrom
hemasekhar-p:springai-tool-calling-fix

Conversation

@hemasekhar-p

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:
When using Spring AI as the model provider in ADK and a tool call is executed, the tool execution result (FunctionResponse part inside ADK Content) was dropped during Prompt construction in MessageConverter.java. On the second request to the LLM, the prompt sent an AssistantMessage containing tool_calls followed by a user message missing the matching tool_call_id response messages. This caused provider API validation failures across LLM providers (e.g., OpenAI HTTP 400 Bad Request: "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'"). Furthermore, MessageConverter generated empty UserMessage("") instances on tool response turns, causing additional validation failures on strict providers

Solution:

  1. Updated MessageConverter.java (handleUserContent) to convert part.functionResponse() into Spring AI ToolResponseMessage
  2. Updated handleUserContent logic so that a UserMessage is only created when non-empty text/media exists or when toolResponseMessages.isEmpty(), preventing empty UserMessage("") instances during tool response turns
  3. Updated MessageConverterTest.java to assert ToolResponseMessage generation

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Please include a summary of passed java test results.

Manual End-to-End (E2E) Tests:

Executed multi-turn tool execution flow with a Spring AI hello-time-agent sample using mock and live model drivers before and after fix to confirm fix works as expected or not.

Checklist

  • I have read the CONTRIBUTING.md document.
  • My pull request contains a single commit.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

@cs1m2414238

Copy link
Copy Markdown

I tested this PR locally against contrib/spring-ai.

I noticed that testToLlmPromptWithFunctionResponse creates a FunctionResponse with id("call_123"), but then uses Part.fromFunctionResponse(name, response), which drops the call ID before MessageConverter receives it.

I updated the test locally to preserve the original FunctionResponse:

Part.builder().functionResponse(functionResponse).build()

and verified that the tool call ID is preserved:

assertThat(response.id()).isEqualTo("call_123");
assertThat(response.name()).isEqualTo("get_weather");

I also added a function-response-only test verifying that no empty UserMessage is emitted.

MessageConverterTest: 32 tests, 0 failures, 0 errors.

I have the additional test coverage pushed to my fork and can open a test-only PR if useful.

@hemasekhar-p
hemasekhar-p force-pushed the springai-tool-calling-fix branch from 2ced4e6 to 4eb6aab Compare August 25, 2026 10:50
ToolResponseMessage toolResponse = (ToolResponseMessage) toolResponseMessage;
assertThat(toolResponse.getResponses()).hasSize(1);
ToolResponseMessage.ToolResponse response = toolResponse.getResponses().get(0);
assertThat(response.name()).isEqualTo("get_weather");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also assert the tool call id? That's the actual subject of #1423, and testToLlmPromptWithFunctionCall already does it for the other direction at line 152.

It looks like it needs a fixture change — Part.fromFunctionResponse(name, response) takes only two arguments, so the .id("call_123") set above never reaches the part and response.id() is currently "". Building it the way line 131 does, keeps the id.

Comment on lines 159 to 164
// TODO: This test is currently limited due to Spring AI 1.1.0 API constraints
// ToolResponseMessage constructors are protected, so function responses are skipped
// Once Spring AI provides public APIs, this test should be updated to verify:
// 1. ToolResponseMessage is created
// 2. Tool response data is properly converted
// 3. Tool call IDs are preserved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we finish the mentioned assertions and already resolve this TODO ?

@@ -184,19 +185,18 @@
Prompt prompt = messageConverter.toLlmPrompt(request);

// Currently only UserMessage is created (function response is skipped)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still correct? Maybe it should be removed?

@hemasekhar-p

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #1459

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool calling problem when using Spring AI

3 participants