This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 97718d7a [integration][ollama] Forward assistant tool calls in the
Java chat history (#1166)
97718d7a is described below
commit 97718d7a66ef75c058f00f5e23526c7607f60984
Author: Edson <[email protected]>
AuthorDate: Tue Sep 29 00:00:26 2026 -0400
[integration][ollama] Forward assistant tool calls in the Java chat history
(#1166)
Generated-by: Claude Code 2.1.259 (Claude Opus 5.5)
---
.../ollama/OllamaChatModelConnection.java | 51 +++++++++++++++++++++-
.../ollama/OllamaChatModelConnectionTest.java | 36 +++++++++++++++
2 files changed, 86 insertions(+), 1 deletion(-)
diff --git
a/integrations/chat-models/ollama/src/main/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnection.java
b/integrations/chat-models/ollama/src/main/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnection.java
index 96187868..36057c95 100644
---
a/integrations/chat-models/ollama/src/main/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnection.java
+++
b/integrations/chat-models/ollama/src/main/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnection.java
@@ -26,6 +26,7 @@ import io.github.ollama4j.exceptions.RoleNotFoundException;
import io.github.ollama4j.models.chat.*;
import io.github.ollama4j.models.request.OllamaChatEndpointCaller;
import io.github.ollama4j.models.request.ThinkMode;
+import io.github.ollama4j.tools.OllamaToolCallsFunction;
import io.github.ollama4j.tools.Tools;
import org.apache.flink.agents.api.chat.messages.ChatMessage;
import org.apache.flink.agents.api.chat.messages.MessageRole;
@@ -174,12 +175,60 @@ public class OllamaChatModelConnection extends
BaseChatModelConnection {
try {
final OllamaChatMessageRole ollamaRole =
OllamaChatMessageRole.getRole(role.name().toLowerCase());
- return new OllamaChatMessage(ollamaRole, message.getText());
+ final OllamaChatMessage ollamaMessage =
+ new OllamaChatMessage(ollamaRole, message.getText());
+ final List<Map<String, Object>> toolCalls = message.getToolCalls();
+ if (toolCalls != null && !toolCalls.isEmpty()) {
+ // Without the calls, the history shows tool results the model
never requested.
+ ollamaMessage.setToolCalls(toOllamaToolCalls(toolCalls));
+ }
+ return ollamaMessage;
} catch (RoleNotFoundException e) {
throw new RuntimeException(e);
}
}
+ /**
+ * Converts framework tool calls back to Ollama's shape, the function name
and its arguments as
+ * an object, as the Python connection does. The framework-assigned id is
not sent.
+ */
+ @SuppressWarnings("unchecked")
+ private static List<OllamaChatToolCalls> toOllamaToolCalls(
+ List<Map<String, Object>> toolCalls) {
+ final List<OllamaChatToolCalls> ollamaToolCalls = new
ArrayList<>(toolCalls.size());
+ for (Map<String, Object> toolCall : toolCalls) {
+ final Map<String, Object> function = (Map<String, Object>)
toolCall.get("function");
+ if (function == null || function.get("name") == null) {
+ throw new IllegalArgumentException("A tool call must have a
function name.");
+ }
+ ollamaToolCalls.add(
+ new OllamaChatToolCalls(
+ null,
+ new OllamaToolCallsFunction(
+ String.valueOf(function.get("name")),
+
toArgumentsMap(function.get("arguments")))));
+ }
+ return ollamaToolCalls;
+ }
+
+ /** Ollama expects the arguments as an object; a JSON string is parsed
into one. */
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> toArgumentsMap(Object arguments) {
+ if (arguments == null) {
+ return Collections.emptyMap();
+ }
+ if (arguments instanceof Map) {
+ return (Map<String, Object>) arguments;
+ }
+ try {
+ return new ObjectMapper()
+ .readValue(
+ String.valueOf(arguments), new
TypeReference<Map<String, Object>>() {});
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Tool call arguments must be a
JSON object.", e);
+ }
+ }
+
/**
* Whether Ollama can constrain generation to a schema for {@code
effectiveModel}.
*
diff --git
a/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnectionTest.java
b/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnectionTest.java
index 3d8a0721..894fd990 100644
---
a/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnectionTest.java
+++
b/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelConnectionTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.ollama4j.models.chat.OllamaChatRequest;
import io.github.ollama4j.tools.Tools;
+import io.github.ollama4j.utils.Utils;
import org.apache.flink.agents.api.chat.messages.ChatMessage;
import org.apache.flink.agents.api.chat.messages.MessageRole;
import org.apache.flink.agents.api.resource.ResourceContext;
@@ -169,6 +170,41 @@ class OllamaChatModelConnectionTest {
.isFalse();
}
+ @Test
+ @DisplayName("Assistant tool calls in the history are sent back to Ollama")
+ void buildRequestForwardsAssistantToolCalls() {
+ Map<String, Object> call =
+ Map.of(
+ "id",
+ "fa-call-1",
+ "type",
+ "function",
+ "function",
+ Map.of("name", "get_weather", "arguments",
Map.of("city", "Paris")));
+ Map<String, Object> jsonArgumentsCall =
+ Map.of("function", Map.of("name", "get_time", "arguments",
"{\"zone\":\"CET\"}"));
+ List<ChatMessage> history =
+ List.of(
+ new ChatMessage(MessageRole.USER, "Weather and time in
Paris?"),
+ ChatMessage.assistant("", List.of(call,
jsonArgumentsCall)),
+ new ChatMessage(MessageRole.TOOL, "sunny"),
+ new ChatMessage(MessageRole.TOOL, "10:00"));
+
+ OllamaChatRequest request =
+ connection().buildRequest(history, List.of(),
params("qwen3:4b"), null);
+ JsonNode wire = Utils.getObjectMapper().valueToTree(request);
+
+ JsonNode toolCalls = wire.at("/messages/1/tool_calls");
+ assertThat(toolCalls).hasSize(2);
+
assertThat(toolCalls.at("/0/function/name").asText()).isEqualTo("get_weather");
+
assertThat(toolCalls.at("/0/function/arguments/city").asText()).isEqualTo("Paris");
+
assertThat(toolCalls.at("/1/function/arguments/zone").asText()).isEqualTo("CET");
+ assertThat(
+ wire.at("/messages/0/tool_calls").isMissingNode()
+ || wire.at("/messages/0/tool_calls").isNull())
+ .isTrue();
+ }
+
@Test
@DisplayName("A POJO output schema is sent as the native format")
void buildRequestSetsFormatForPojoSchema() {