From 9bbf53122d8369cb34d36787486f22fd9ed88459 Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Mon, 27 Apr 2026 23:42:53 +0200 Subject: [PATCH 01/24] 1652 Order advisors and wire ToolCallAdvisor in AI agent --- .../memory/cluster/VectorStoreChatMemory.java | 4 ++- .../ai/agent/AiAgentComponentHandler.java | 8 +++-- .../action/AbstractAiAgentChatAction.java | 31 ++++++++++++++++--- .../ai/agent/action/AiAgentChatAction.java | 14 ++++++--- .../agent/action/AiAgentStreamChatAction.java | 12 ++++--- .../action/AbstractAiAgentChatActionTest.java | 17 +++++++--- .../chat/AiAgentComponentHandlerTest.java | 2 +- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java index 421d1808957..98b9452f2f3 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java @@ -33,6 +33,7 @@ import com.bytechef.platform.configuration.domain.ClusterElement; import com.bytechef.platform.configuration.domain.ClusterElementMap; import java.util.Map; +import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor.Builder; @@ -97,7 +98,8 @@ protected ChatMemoryFunction.Result apply( ParametersFactory.create(componentConnectionConnectionParameters), ParametersFactory.create(clusterElement.getExtensions()), componentConnections)) .defaultTopK( - inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)); + inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)) + .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200); return new ChatMemoryFunction.Result(builder.build(), null); } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java index 6c39177c9fc..3407ea47739 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java @@ -30,6 +30,7 @@ import com.bytechef.platform.component.definition.AbstractComponentDefinitionWrapper; import com.bytechef.platform.component.definition.AiAgentComponentDefinition; import com.bytechef.platform.component.service.ClusterElementDefinitionService; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.stereotype.Component; /** @@ -41,10 +42,11 @@ public class AiAgentComponentHandler implements ComponentHandler { private final AiAgentComponentDefinition componentDefinition; public AiAgentComponentHandler( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade) { + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + ToolCallingManager toolCallingManager) { final ActionDefinition aiAgentChatActionDefinition = - AiAgentChatAction.of(clusterElementDefinitionService, aiAgentToolFacade); + AiAgentChatAction.of(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); this.componentDefinition = new AiAgentComponentDefinitionImpl( component(AI_AGENT) @@ -54,7 +56,7 @@ public AiAgentComponentHandler( .categories(ComponentCategory.ARTIFICIAL_INTELLIGENCE) .actions( aiAgentChatActionDefinition, - AiAgentStreamChatAction.of(clusterElementDefinitionService, aiAgentToolFacade)) + AiAgentStreamChatAction.of(clusterElementDefinitionService, aiAgentToolFacade, toolCallingManager)) .clusterElements(AiAgentChatTool.of(aiAgentChatActionDefinition))); } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 6688202ba74..1dda3b987f3 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -68,12 +68,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; +import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.augment.AugmentedToolCallbackProvider; import org.springframework.ai.tool.definition.ToolDefinition; @@ -91,12 +94,15 @@ public abstract class AbstractAiAgentChatAction { private final ClusterElementDefinitionService clusterElementDefinitionService; private final AiAgentToolFacade aiAgentToolFacade; + private final ToolCallingManager toolCallingManager; protected AbstractAiAgentChatAction( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade) { + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + ToolCallingManager toolCallingManager) { - this.clusterElementDefinitionService = clusterElementDefinitionService; this.aiAgentToolFacade = aiAgentToolFacade; + this.clusterElementDefinitionService = clusterElementDefinitionService; + this.toolCallingManager = toolCallingManager; } protected ChatClient.ChatClientRequestSpec getChatClientRequestSpec( @@ -340,8 +346,25 @@ private List getAdvisors( } } - chatMemoryResult.map(ChatMemoryFunction.Result::advisor) - .ifPresent(advisors::add); + // tool call + + ToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolCallAdvisor.builder() + .toolCallingManager(toolCallingManager) + .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300); + + // memory + + Advisor chatMemoryAdvisor = chatMemoryResult + .map(ChatMemoryFunction.Result::advisor) + .orElse(null); + + if (chatMemoryAdvisor != null) { + advisors.add(chatMemoryAdvisor); + + toolCallAdvisorBuilder.disableInternalConversationHistory(); + } + + advisors.add(toolCallAdvisorBuilder.build()); clusterElementMap.fetchClusterElement(RAG) .map(clusterElement -> getRagAdvisor(connectionParameters, clusterElement, context)) diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java index af47aaaab83..7e5fdb6f35c 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java @@ -42,6 +42,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient.ChatClientRequestSpec; +import org.springframework.ai.model.tool.ToolCallingManager; /** * @author Ivica Cardic @@ -49,14 +50,17 @@ public class AiAgentChatAction extends AbstractAiAgentChatAction { public static ChatActionDefinitionWrapper of( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade) { + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + ToolCallingManager toolCallingManager) { - return new AiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade).build(); + return new AiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade, toolCallingManager).build(); } - private AiAgentChatAction(ClusterElementDefinitionService clusterElementDefinitionService, - AiAgentToolFacade aiAgentToolFacade) { - super(clusterElementDefinitionService, aiAgentToolFacade); + private AiAgentChatAction( + ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade, + ToolCallingManager toolCallingManager) { + + super(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); } private ChatActionDefinitionWrapper build() { diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java index f30131a227f..88082eda39e 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java @@ -48,6 +48,7 @@ import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.model.tool.ToolCallingManager; import reactor.core.publisher.Flux; /** @@ -56,15 +57,18 @@ public class AiAgentStreamChatAction extends AbstractAiAgentChatAction { public static ActionDefinition of( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade) { + ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade, + ToolCallingManager toolCallingManager) { - return new AiAgentStreamChatAction(clusterElementDefinitionService, aiAgentToolFacade).build(); + return new AiAgentStreamChatAction(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) + .build(); } private AiAgentStreamChatAction( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade) { + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + ToolCallingManager toolCallingManager) { - super(clusterElementDefinitionService, aiAgentToolFacade); + super(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); } private ChatActionDefinitionWrapper build() { diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index ebab86cb558..d7b7cc03ad2 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -49,6 +49,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.model.tool.ToolCallingManager; /** * @author Ivica Cardic @@ -56,11 +57,14 @@ @ExtendWith(MockitoExtension.class) class AbstractAiAgentChatActionTest { + @Mock + private AiAgentToolFacade aiAgentToolFacade; + @Mock private ClusterElementDefinitionService clusterElementDefinitionService; @Mock - private AiAgentToolFacade aiAgentToolFacade; + private ToolCallingManager toolCallingManager; @Test void testGetChatClientRequestSpecWithNullParameterValues() throws Exception { @@ -113,7 +117,8 @@ void testGetChatClientRequestSpecWithNullParameterValues() throws Exception { ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -386,9 +391,11 @@ private void stubModelLookup() throws Exception { private static class TestAiAgentChatAction extends AbstractAiAgentChatAction { - TestAiAgentChatAction(ClusterElementDefinitionService clusterElementDefinitionService, - AiAgentToolFacade aiAgentToolFacade) { - super(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction( + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + ToolCallingManager toolCallingManager) { + + super(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); } } } diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/chat/AiAgentComponentHandlerTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/chat/AiAgentComponentHandlerTest.java index 47dcf59d080..89847b00f15 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/chat/AiAgentComponentHandlerTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/chat/AiAgentComponentHandlerTest.java @@ -25,6 +25,6 @@ public class AiAgentComponentHandlerTest { @Test public void testGetComponentDefinition() { JsonFileAssert.assertEquals( - "definition/ai-agent_v1.json", new AiAgentComponentHandler(null, null).getDefinition()); + "definition/ai-agent_v1.json", new AiAgentComponentHandler(null, null, null).getDefinition()); } } From b0044add611e8af17718d92494fda230cec8e2fa Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Fri, 8 May 2026 17:01:23 +0200 Subject: [PATCH 02/24] 1652 Align AiAgent action factory parameter order and test ToolCallAdvisor wiring Standardize (AiAgentToolFacade, ClusterElementDefinitionService, ToolCallingManager) across AiAgentChatAction.of, AiAgentStreamChatAction.of, their constructors, and the AiAgentComponentHandler call sites. Add tests asserting ToolCallAdvisor is present in the advisor list and that disableInternalConversationHistory() is applied only when a chat-memory advisor is configured. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ai/agent/AiAgentComponentHandler.java | 2 +- .../action/AbstractAiAgentChatAction.java | 2 +- .../ai/agent/action/AiAgentChatAction.java | 4 +- .../agent/action/AiAgentStreamChatAction.java | 2 +- .../action/AbstractAiAgentChatActionTest.java | 115 ++++++++++++++++-- 5 files changed, 113 insertions(+), 12 deletions(-) diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java index 3407ea47739..932d1be030a 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/AiAgentComponentHandler.java @@ -56,7 +56,7 @@ public AiAgentComponentHandler( .categories(ComponentCategory.ARTIFICIAL_INTELLIGENCE) .actions( aiAgentChatActionDefinition, - AiAgentStreamChatAction.of(clusterElementDefinitionService, aiAgentToolFacade, toolCallingManager)) + AiAgentStreamChatAction.of(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager)) .clusterElements(AiAgentChatTool.of(aiAgentChatActionDefinition))); } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 1dda3b987f3..acc0eb5578c 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -303,7 +303,7 @@ public String call(String toolInput, @Nullable ToolContext toolContext) { }; } - private List getAdvisors( + List getAdvisors( ClusterElementMap clusterElementMap, Map connectionParameters, ActionContext context) { diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java index 7e5fdb6f35c..6307958eb33 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentChatAction.java @@ -53,11 +53,11 @@ public static ChatActionDefinitionWrapper of( AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, ToolCallingManager toolCallingManager) { - return new AiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade, toolCallingManager).build(); + return new AiAgentChatAction(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager).build(); } private AiAgentChatAction( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade, + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, ToolCallingManager toolCallingManager) { super(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java index 88082eda39e..39c10a402da 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AiAgentStreamChatAction.java @@ -57,7 +57,7 @@ public class AiAgentStreamChatAction extends AbstractAiAgentChatAction { public static ActionDefinition of( - ClusterElementDefinitionService clusterElementDefinitionService, AiAgentToolFacade aiAgentToolFacade, + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, ToolCallingManager toolCallingManager) { return new AiAgentStreamChatAction(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index d7b7cc03ad2..4b601ea8540 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -39,6 +39,8 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import com.bytechef.platform.component.definition.ai.agent.ModelFunction; import com.bytechef.platform.component.service.ClusterElementDefinitionService; +import com.bytechef.platform.configuration.domain.ClusterElementMap; +import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,6 +49,8 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; +import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.model.tool.ToolCallingManager; @@ -153,7 +157,8 @@ void testMultipleCheckForViolationsRejectedAtAdvisorBuild() throws Exception { Map connectionParameters = Map.of("model_1", componentConnection); ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -190,7 +195,8 @@ void testMultipleSanitizeTextRejectedAtAdvisorBuild() throws Exception { Map connectionParameters = Map.of("model_1", componentConnection); ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -229,8 +235,7 @@ void testSingleCheckForViolationsAndSingleSanitizeTextAccepted() throws Exceptio com.bytechef.platform.component.definition.ai.agent.GuardrailsFunction guardrailsFunction = mock( com.bytechef.platform.component.definition.ai.agent.GuardrailsFunction.class); - org.springframework.ai.chat.client.advisor.api.Advisor advisor = mock( - org.springframework.ai.chat.client.advisor.api.Advisor.class); + Advisor advisor = mock(Advisor.class); when( clusterElementDefinitionService.getClusterElement( @@ -253,7 +258,8 @@ void testSingleCheckForViolationsAndSingleSanitizeTextAccepted() throws Exceptio ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -305,7 +311,8 @@ void testGuardrailAdvisorBuildFailureInvokesContextLog() throws Exception { ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -341,7 +348,8 @@ void testNoGuardrailsConfiguredRunsModelChainWithoutAddingAdvisor() throws Excep ActionContext actionContext = mock(ActionContext.class); - TestAiAgentChatAction action = new TestAiAgentChatAction(clusterElementDefinitionService, aiAgentToolFacade); + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) @@ -371,6 +379,76 @@ private static Map buildModelElement() { return modelElement; } + @Test + void testGetAdvisorsIncludesToolCallAdvisorWithDefaultConversationHistoryWhenNoChatMemory() { + ClusterElementMap clusterElementMap = ClusterElementMap.of( + Map.of("clusterElements", Map.of("model", buildModelClusterElement()))); + + ActionContext actionContext = mock(ActionContext.class); + + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); + + List advisors = action.getAdvisors(clusterElementMap, Map.of(), actionContext); + + ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); + + assertThat(toolCallAdvisor).isNotNull(); + assertThat(advisors).noneMatch(BaseChatMemoryAdvisor.class::isInstance); + assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isTrue(); + } + + @Test + void testGetAdvisorsAddsChatMemoryBeforeToolCallAdvisorAndDisablesInternalConversationHistory() throws Exception { + Map chatMemoryElement = new HashMap<>(); + + chatMemoryElement.put("name", "memory_1"); + chatMemoryElement.put("type", "memoryComponent/v1/memoryElement"); + chatMemoryElement.put("parameters", Map.of()); + + ClusterElementMap clusterElementMap = ClusterElementMap.of( + Map.of( + "clusterElements", + Map.of("model", buildModelClusterElement(), "chatMemory", chatMemoryElement))); + + BaseChatMemoryAdvisor chatMemoryAdvisor = mock(BaseChatMemoryAdvisor.class); + + ChatMemoryFunction chatMemoryFunction = mock(ChatMemoryFunction.class); + + when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(chatMemoryAdvisor); + when(clusterElementDefinitionService.getClusterElement( + eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); + + ComponentConnection memoryConnection = new ComponentConnection( + "memoryComponent", 1, 2L, Map.of(), null); + + Map connectionParameters = Map.of("memory_1", memoryConnection); + ActionContext actionContext = mock(ActionContext.class); + + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); + + List advisors = action.getAdvisors(clusterElementMap, connectionParameters, actionContext); + + int chatMemoryIndex = advisors.indexOf(chatMemoryAdvisor); + ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); + int toolCallIndex = advisors.indexOf(toolCallAdvisor); + + assertThat(chatMemoryIndex).isGreaterThanOrEqualTo(0); + assertThat(toolCallIndex).isGreaterThan(chatMemoryIndex); + assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isFalse(); + } + + private static Map buildModelClusterElement() { + Map modelElement = new HashMap<>(); + + modelElement.put("name", "model_1"); + modelElement.put("type", "testComponent/v1/testModel"); + modelElement.put("parameters", Map.of()); + + return modelElement; + } + private static Map buildGuardrailElement(String workflowNodeName, String type) { Map element = new HashMap<>(); element.put("name", workflowNodeName); @@ -389,6 +467,29 @@ private void stubModelLookup() throws Exception { when(modelFunction.apply(any(), any(), anyBoolean())).thenAnswer(invocation -> chatModel); } + private static ToolCallAdvisor findToolCallAdvisor(List advisors) { + return advisors.stream() + .filter(ToolCallAdvisor.class::isInstance) + .map(ToolCallAdvisor.class::cast) + .findFirst() + .orElseThrow(() -> new AssertionError("Expected ToolCallAdvisor in advisor list")); + } + + private static boolean readConversationHistoryEnabled(ToolCallAdvisor toolCallAdvisor) { + try { + Field field = ToolCallAdvisor.class.getDeclaredField("conversationHistoryEnabled"); + + field.setAccessible(true); + + return field.getBoolean(toolCallAdvisor); + } catch (ReflectiveOperationException exception) { + throw new AssertionError( + "Unable to read conversationHistoryEnabled from ToolCallAdvisor — " + + "field name changed in Spring AI?", + exception); + } + } + private static class TestAiAgentChatAction extends AbstractAiAgentChatAction { TestAiAgentChatAction( From 770e7bf49a059f0d62c239d7d0d79acd1356e0cd Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Fri, 15 May 2026 16:46:50 +0200 Subject: [PATCH 03/24] 1652 - ordering by DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER --- .../ai/chat/memory/cluster/VectorStoreChatMemory.java | 3 +-- .../component/ai/agent/action/AbstractAiAgentChatAction.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java index 98b9452f2f3..07c4ad643d3 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java @@ -98,8 +98,7 @@ protected ChatMemoryFunction.Result apply( ParametersFactory.create(componentConnectionConnectionParameters), ParametersFactory.create(clusterElement.getExtensions()), componentConnections)) .defaultTopK( - inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200); + inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)); return new ChatMemoryFunction.Result(builder.build(), null); } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index acc0eb5578c..b24c273478e 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -350,7 +350,7 @@ List getAdvisors( ToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolCallAdvisor.builder() .toolCallingManager(toolCallingManager) - .advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300); + .advisorOrder(BaseAdvisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER + 100); // memory From a3cab9597e989c49db47ede3cf08b97991c4169a Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 11:14:56 +0200 Subject: [PATCH 04/24] 1652 Place ChatMemoryAdvisor downstream of ToolCallAdvisor so memory participates in every tool-call iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous setup ran every chat-memory advisor at HIGHEST_PRECEDENCE + 200 and ToolCallAdvisor at HIGHEST_PRECEDENCE + 300 — Spring's lower-order-runs-first contract meant memory ran ONCE outside the tool-call loop. With disableInternalConversationHistory() enabled, ToolCallAdvisor passes only [system, lastMessage] between loop iterations and expects a downstream ChatMemoryAdvisor to supply the preceding assistant-with-tool-calls. Because no advisor was downstream, iteration 2 reached the model as just [system, toolResultMsg], producing the OpenAI 400 "messages with role 'tool' must be a response to a preceding message with 'tool_calls'" error. Drop the explicit orders on both sides so each advisor falls back to its Spring AI default: ToolCallAdvisor at HIGHEST_PRECEDENCE + 300 (its DEFAULT_ORDER) and the seven non-vector chat-memory advisors at DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER (Integer.MIN_VALUE + 1000). The vector-store advisor already uses the default after commit 37fb7aa8ae3. Spring AI documents these defaults as designed to compose — ToolCallAdvisor.DEFAULT_ORDER's Javadoc states "Placed early in the chain so that all downstream advisors (e.g. BaseChatMemoryAdvisor) participate in every tool-call iteration." Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ai/agent/chat/memory/builtin/cluster/ChatMemory.java | 2 -- .../chat/memory/cassandra/cluster/CassandraChatMemory.java | 2 -- .../agent/chat/memory/memory/cluster/InMemoryChatMemory.java | 2 -- .../ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java | 2 -- .../agent/chat/memory/mongodb/cluster/MongoDbChatMemory.java | 2 -- .../ai/agent/chat/memory/neo4j/cluster/Neo4jChatMemory.java | 2 -- .../ai/agent/chat/memory/redis/cluster/RedisChatMemory.java | 2 -- .../ai/chat/memory/cluster/VectorStoreChatMemory.java | 1 - .../component/ai/agent/action/AbstractAiAgentChatAction.java | 4 +--- 9 files changed, 1 insertion(+), 18 deletions(-) diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java index 8829d7d0a0d..73ca369496f 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java @@ -26,7 +26,6 @@ import com.bytechef.component.definition.Parameters; import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; @@ -63,7 +62,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cassandra/src/main/java/com/bytechef/component/ai/agent/chat/memory/cassandra/cluster/CassandraChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cassandra/src/main/java/com/bytechef/component/ai/agent/chat/memory/cassandra/cluster/CassandraChatMemory.java index 2d6ea9b121b..5aa15058a97 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cassandra/src/main/java/com/bytechef/component/ai/agent/chat/memory/cassandra/cluster/CassandraChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cassandra/src/main/java/com/bytechef/component/ai/agent/chat/memory/cassandra/cluster/CassandraChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -63,7 +62,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-in-memory/src/main/java/com/bytechef/component/ai/agent/chat/memory/memory/cluster/InMemoryChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-in-memory/src/main/java/com/bytechef/component/ai/agent/chat/memory/memory/cluster/InMemoryChatMemory.java index c4ceea3763b..10dc7114706 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-in-memory/src/main/java/com/bytechef/component/ai/agent/chat/memory/memory/cluster/InMemoryChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-in-memory/src/main/java/com/bytechef/component/ai/agent/chat/memory/memory/cluster/InMemoryChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -61,7 +60,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(inMemoryChatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), inMemoryChatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java index 54baeb4c94a..6a99fb953dd 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.service.ClusterElementDefinitionService; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -75,7 +74,6 @@ protected ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-mongodb/src/main/java/com/bytechef/component/ai/agent/chat/memory/mongodb/cluster/MongoDbChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-mongodb/src/main/java/com/bytechef/component/ai/agent/chat/memory/mongodb/cluster/MongoDbChatMemory.java index 9892bbd178c..b06c15f3a01 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-mongodb/src/main/java/com/bytechef/component/ai/agent/chat/memory/mongodb/cluster/MongoDbChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-mongodb/src/main/java/com/bytechef/component/ai/agent/chat/memory/mongodb/cluster/MongoDbChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -60,7 +59,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-neo4j/src/main/java/com/bytechef/component/ai/agent/chat/memory/neo4j/cluster/Neo4jChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-neo4j/src/main/java/com/bytechef/component/ai/agent/chat/memory/neo4j/cluster/Neo4jChatMemory.java index fd1fbf89c9b..8bd6072628b 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-neo4j/src/main/java/com/bytechef/component/ai/agent/chat/memory/neo4j/cluster/Neo4jChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-neo4j/src/main/java/com/bytechef/component/ai/agent/chat/memory/neo4j/cluster/Neo4jChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -63,7 +62,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-redis/src/main/java/com/bytechef/component/ai/agent/chat/memory/redis/cluster/RedisChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-redis/src/main/java/com/bytechef/component/ai/agent/chat/memory/redis/cluster/RedisChatMemory.java index ab43d246fa5..457d333f854 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-redis/src/main/java/com/bytechef/component/ai/agent/chat/memory/redis/cluster/RedisChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-redis/src/main/java/com/bytechef/component/ai/agent/chat/memory/redis/cluster/RedisChatMemory.java @@ -29,7 +29,6 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import java.util.Map; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; /** @@ -60,7 +59,6 @@ protected static ChatMemoryFunction.Result apply( return new ChatMemoryFunction.Result( MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java index 07c4ad643d3..421d1808957 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java @@ -33,7 +33,6 @@ import com.bytechef.platform.configuration.domain.ClusterElement; import com.bytechef.platform.configuration.domain.ClusterElementMap; import java.util.Map; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor.Builder; diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index b24c273478e..538f750f688 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -70,7 +70,6 @@ import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; @@ -349,8 +348,7 @@ List getAdvisors( // tool call ToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolCallAdvisor.builder() - .toolCallingManager(toolCallingManager) - .advisorOrder(BaseAdvisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER + 100); + .toolCallingManager(toolCallingManager); // memory From 6c6ec1a058fc4f56a0031412c2218ed6f11c2607 Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 11:35:47 +0200 Subject: [PATCH 05/24] 1652 Add regression tests pinning ChatMemoryAdvisor ordering relative to ToolCallAdvisor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testChatMemoryAdvisorOrderedDownstreamOfToolCallAdvisor builds the agent's advisor chain end-to-end with a production-style MessageChatMemoryAdvisor (built via the Spring AI builder default, the same way every chat-memory component now constructs it) and asserts its order is greater than the ToolCallAdvisor's. Verified by hand that this test fails when the previous buggy ordering is restored — catching the exact regression that produced OpenAI's "messages with role 'tool' must be a response to a preceding message with 'tool_calls'" failure. testSpringAiDefaultOrdersComposeCorrectly pins the Spring AI defaults directly (ToolCallAdvisor.builder().build().getOrder() vs MessageChatMemoryAdvisor.builder(...).build().getOrder()) so a future spring-ai version bump that inverts those defaults fails the test at the bump PR instead of producing chat-memory breakage in production. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../action/AbstractAiAgentChatActionTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index 4b601ea8540..927f67459f2 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -49,9 +49,13 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.DefaultChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.model.tool.ToolCallingManager; @@ -367,6 +371,104 @@ void testNoGuardrailsConfiguredRunsModelChainWithoutAddingAdvisor() throws Excep eq("sanitizeText"), anyInt(), anyString()); } + @Test + void testChatMemoryAdvisorOrderedDownstreamOfToolCallAdvisor() throws Exception { + // Regression for the M7 ordering bug: with disableInternalConversationHistory() active, + // ToolCallAdvisor passes only [systemMsg, lastMessage] between loop iterations and relies on a + // DOWNSTREAM ChatMemoryAdvisor to rehydrate the preceding assistant-with-tool-calls each turn. + // If a future change places ChatMemoryAdvisor at a lower order than ToolCallAdvisor, memory + // moves outside the tool-call loop and the second model call lands at OpenAI as + // [systemMsg, toolResultMsg], producing + // "messages with role 'tool' must be a response to a preceding message with 'tool_calls'". + Parameters inputParameters = MockParametersFactory.create(Map.of()); + + Map modelElement = buildModelElement(); + + Map chatMemoryParameters = new HashMap<>(); + chatMemoryParameters.put("conversationId", "test-conversation"); + + Map chatMemoryElement = new HashMap<>(); + chatMemoryElement.put("name", "chatMemory_1"); + chatMemoryElement.put("type", "testComponent/v1/testChatMemory"); + chatMemoryElement.put("parameters", chatMemoryParameters); + + Parameters extensions = MockParametersFactory.create( + Map.of("clusterElements", Map.of("model", modelElement, "chatMemory", chatMemoryElement))); + + stubModelLookup(); + + ChatMemoryFunction chatMemoryFunction = mock(ChatMemoryFunction.class); + + when(clusterElementDefinitionService.getClusterElement( + eq("testComponent"), eq(1), eq("testChatMemory"))).thenReturn(chatMemoryFunction); + + // Build the chat-memory advisor exactly as the production chat-memory components do + // (default order via the builder), so the assertion catches regressions in any of them. + MessageChatMemoryAdvisor productionStyleChatMemoryAdvisor = MessageChatMemoryAdvisor + .builder(mock(ChatMemory.class)) + .build(); + + when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(productionStyleChatMemoryAdvisor); + + ComponentConnection componentConnection = new ComponentConnection( + "testComponent", 1, 1L, Map.of(), null); + Map connectionParameters = Map.of( + "model_1", componentConnection, + "chatMemory_1", componentConnection); + + ActionContext actionContext = mock(ActionContext.class); + + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); + + try (MockedStatic modelUtilsMockedStatic = mockStatic(ModelUtils.class)) { + modelUtilsMockedStatic.when(() -> ModelUtils.getMessages(any(), any())) + .thenReturn(List.of()); + + ChatClient.ChatClientRequestSpec spec = action.getChatClientRequestSpec( + inputParameters, connectionParameters, extensions, null, actionContext); + + List advisors = ((DefaultChatClient.DefaultChatClientRequestSpec) spec).getAdvisors(); + + Advisor toolCallAdvisor = advisors.stream() + .filter(ToolCallAdvisor.class::isInstance) + .findFirst() + .orElseThrow(() -> new AssertionError("ToolCallAdvisor missing from advisor chain")); + + Advisor chatMemoryAdvisor = advisors.stream() + .filter(advisor -> advisor instanceof BaseChatMemoryAdvisor) + .findFirst() + .orElseThrow(() -> new AssertionError("ChatMemoryAdvisor missing from advisor chain")); + + assertThat(chatMemoryAdvisor.getOrder()) + .as( + "ChatMemoryAdvisor MUST run downstream of ToolCallAdvisor so memory participates in " + + "every tool-call iteration. With disableInternalConversationHistory() active, the " + + "inverse ordering causes OpenAI to reject the second iteration's prompt because the " + + "tool-result message has no preceding assistant-with-tool-calls.") + .isGreaterThan(toolCallAdvisor.getOrder()); + } + } + + @Test + void testSpringAiDefaultOrdersComposeCorrectly() { + // Safety net for Spring AI version bumps. Our chat-memory components and AbstractAiAgentChatAction + // both rely on Spring AI's builder defaults (no explicit .order(...) / .advisorOrder(...) calls) + // to produce ToolCallAdvisor < ChatMemoryAdvisor. If a future bump inverts these defaults, every + // chat-memory-backed agent breaks with the same OpenAI tool-message ordering error — this test + // surfaces the regression at the dependency-bump PR rather than in production. + ToolCallAdvisor toolCallAdvisor = ToolCallAdvisor.builder() + .build(); + + MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor + .builder(mock(ChatMemory.class)) + .build(); + + assertThat(chatMemoryAdvisor.getOrder()) + .as("Spring AI default ordering must keep ChatMemoryAdvisor downstream of ToolCallAdvisor") + .isGreaterThan(toolCallAdvisor.getOrder()); + } + private static Map buildModelElement() { HashMap modelParams = new HashMap<>(); modelParams.put("model", "gpt-4o"); From b5b9fbcf2cfddf5aa40938fc1ec3465ddefe2f2b Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 18:10:40 +0200 Subject: [PATCH 06/24] 1652 Document why disableInternalConversationHistory() is required on manually-registered ToolCallAdvisor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring AI 2.0's docs make this call look optional or removable — auto-registered ToolCallAdvisors get the auto-disable behavior when a downstream ChatMemoryAdvisor is detected. Manually-registered ones (which we need because SuspendableToolCallingManager wraps the default ToolCallingManager) skip that path entirely, so the explicit call is load-bearing. Comment the WHY so a future reader doesn't reach for the obvious-looking deletion. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../component/ai/agent/action/AbstractAiAgentChatAction.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 538f750f688..ca330c90d7d 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -359,6 +359,11 @@ List getAdvisors( if (chatMemoryAdvisor != null) { advisors.add(chatMemoryAdvisor); + // Spring AI auto-applies this when ToolCallAdvisor is auto-registered (see DefaultChatClient + // .autoRegisterToolCallAdvisor), but we register manually to supply our own ToolCallingManager, + // so the auto-disable path is skipped and we must call it ourselves. Removing this line makes + // ToolCallAdvisor carry full conversation history alongside the downstream ChatMemoryAdvisor — + // double bookkeeping that produces malformed tool-call sequences on OpenAI. toolCallAdvisorBuilder.disableInternalConversationHistory(); } From b85c12dfbb2bbbf51dd8b3306db706a0fb72a363 Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 07:54:49 +0200 Subject: [PATCH 07/24] 1652 Update Spring AI to 2.0.0-M7 M7 removes spring-ai-model-chat-memory-repository-cosmos-db (#6080), so delete the chat-memory-cosmosdb component along with its docs row and generated reference page. M7's "Sanitize Spring Boot related dependencies" (#6088) also stops leaking spring-boot-jdbc and spring-boot-autoconfigure transitively, so declare them explicitly in the two modules that were relying on the leak. The custom ToolCallAdvisor registration in AbstractAiAgentChatAction stays as-is: manual registration suppresses M7's new auto-registration (#5459), and SuspendableToolCallingManager requires the explicit toolCallingManager wiring that auto-registration cannot provide. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/cosmos-db-chat-memory_v1.mdx | 226 ---- gradle/libs.versions.toml | 2 +- .../build.gradle.kts | 1 + .../chat-memory-cosmosdb/build.gradle.kts | 3 - .../CosmosDbChatMemoryComponentHandler.java | 91 -- .../CosmosDbChatMemoryAddMessagesAction.java | 112 -- .../CosmosDbChatMemoryDeleteAction.java | 72 -- .../CosmosDbChatMemoryGetMessagesAction.java | 104 -- ...osDbChatMemoryListConversationsAction.java | 64 -- .../cosmosdb/cluster/CosmosDbChatMemory.java | 70 -- .../constant/CosmosDbChatMemoryConstants.java | 32 - .../util/CosmosDbChatMemoryUtils.java | 84 -- .../resources/assets/cosmosdb-chat-memory.svg | 1 - ...osmosDbChatMemoryComponentHandlerTest.java | 29 - .../definition/cosmos-db-chat-memory_v1.json | 994 ------------------ .../build.gradle.kts | 1 + settings.gradle.kts | 1 - 17 files changed, 3 insertions(+), 1884 deletions(-) delete mode 100644 docs/content/docs/reference/components/cosmos-db-chat-memory_v1.mdx delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/build.gradle.kts delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandler.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryAddMessagesAction.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryDeleteAction.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryGetMessagesAction.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryListConversationsAction.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/cluster/CosmosDbChatMemory.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/constant/CosmosDbChatMemoryConstants.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/util/CosmosDbChatMemoryUtils.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/resources/assets/cosmosdb-chat-memory.svg delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandlerTest.java delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/resources/definition/cosmos-db-chat-memory_v1.json diff --git a/docs/content/docs/reference/components/cosmos-db-chat-memory_v1.mdx b/docs/content/docs/reference/components/cosmos-db-chat-memory_v1.mdx deleted file mode 100644 index 29534b8f1a4..00000000000 --- a/docs/content/docs/reference/components/cosmos-db-chat-memory_v1.mdx +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: "Cosmos DB Chat Memory" -description: "Cosmos DB Chat Memory stores conversation history in Azure Cosmos DB for globally distributed, scalable persistent storage." ---- - - -Categories: Artificial Intelligence - - -Type: cosmosDbChatMemory/v1 - -
- - - - -## Connections - -Version: 1 - - -### custom - -#### Properties - -| Name | Label | Type | Description | Required | -|:---------------:|:--------------:|:------------:|:-------------------:|:--------:| -| key | Key | STRING | The Azure Cosmos DB account key. | true | - - - - - - -
- - -## Actions - - -### Add Messages -Name: addMessages - -`Adds messages to the chat memory for a conversation.` - -#### Properties - -| Name | Label | Type | Description | Required | -|:---------------:|:--------------:|:------------:|:-------------------:|:--------:| -| conversationId | Conversation ID | STRING | The unique identifier for the conversation. | true | -| messages | Messages | ARRAY
Items [{STRING\(role), STRING\(content)}]
| The messages to add to the conversation. | true | - -#### Example JSON Structure -```json -{ - "label" : "Add Messages", - "name" : "addMessages", - "parameters" : { - "conversationId" : "", - "messages" : [ { - "role" : "", - "content" : "" - } ] - }, - "type" : "cosmosDbChatMemory/v1/addMessages" -} -``` - -#### Output - -This action does not produce any output. - - - - - - -### Get Messages -Name: getMessages - -`Retrieves all messages from a conversation.` - -#### Properties - -| Name | Label | Type | Description | Required | -|:---------------:|:--------------:|:------------:|:-------------------:|:--------:| -| conversationId | Conversation ID | STRING | The unique identifier for the conversation. | true | - -#### Example JSON Structure -```json -{ - "label" : "Get Messages", - "name" : "getMessages", - "parameters" : { - "conversationId" : "" - }, - "type" : "cosmosDbChatMemory/v1/getMessages" -} -``` - -#### Output - - - -Type: OBJECT - - -#### Properties - -| Name | Type | Description | -|:------------:|:------------:|:-------------------:| -| conversationId | STRING | | -| messages | ARRAY
Items [{STRING\(role), STRING\(content)}]
| | - - - - -#### Output Example -```json -{ - "conversationId" : "", - "messages" : [ { - "role" : "", - "content" : "" - } ] -} -``` - - - - -### Delete Conversation -Name: deleteConversation - -`Deletes all messages for a conversation.` - -#### Properties - -| Name | Label | Type | Description | Required | -|:---------------:|:--------------:|:------------:|:-------------------:|:--------:| -| conversationId | Conversation ID | STRING | The unique identifier for the conversation to delete. | true | - -#### Example JSON Structure -```json -{ - "label" : "Delete Conversation", - "name" : "deleteConversation", - "parameters" : { - "conversationId" : "" - }, - "type" : "cosmosDbChatMemory/v1/deleteConversation" -} -``` - -#### Output - - - -Type: OBJECT - - -#### Properties - -| Name | Type | Description | -|:------------:|:------------:|:-------------------:| -| conversationId | STRING | | -| deleted | BOOLEAN
Options true, false
| | - - - - -#### Output Example -```json -{ - "conversationId" : "", - "deleted" : false -} -``` - - - - -### List Conversations -Name: listConversations - -`Lists all conversation IDs in the chat memory.` - -#### Example JSON Structure -```json -{ - "label" : "List Conversations", - "name" : "listConversations", - "type" : "cosmosDbChatMemory/v1/listConversations" -} -``` - -#### Output - - - -Type: OBJECT - - -#### Properties - -| Name | Type | Description | -|:------------:|:------------:|:-------------------:| -| conversationIds | ARRAY
Items [STRING]
| | -| count | INTEGER | | - - - - -#### Output Example -```json -{ - "conversationIds" : [ "" ], - "count" : 1 -} -``` - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34297dd8329..7d05ce64654 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ org-mapstruct-extensions-spring = "2.0.0" org-springdoc = "3.0.3" pmd = "7.23.0" spotbugs = "4.9.8" -spring-ai = "2.0.0-M6" +spring-ai = "2.0.0-M7" spring-boot = "4.0.6" spring-cloud-aws = "4.0.2" spring-cloud-dependencies = "2025.1.1" diff --git a/server/ee/libs/config/tenant-multi-pgvector-config/build.gradle.kts b/server/ee/libs/config/tenant-multi-pgvector-config/build.gradle.kts index 763819a7005..f92a69cbe72 100644 --- a/server/ee/libs/config/tenant-multi-pgvector-config/build.gradle.kts +++ b/server/ee/libs/config/tenant-multi-pgvector-config/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { implementation("org.springframework.ai:spring-ai-autoconfigure-vector-store-pgvector") implementation("org.springframework.ai:spring-ai-pgvector-store") implementation("org.springframework.boot:spring-boot-autoconfigure") + implementation("org.springframework.boot:spring-boot-jdbc") implementation("org.springframework.data:spring-data-jdbc") implementation("org.springframework:spring-jdbc") implementation("org.springframework:spring-tx") diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/build.gradle.kts b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/build.gradle.kts deleted file mode 100644 index 056ce1e3c58..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - implementation("org.springframework.ai:spring-ai-model-chat-memory-repository-cosmos-db") -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandler.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandler.java deleted file mode 100644 index 13fa5696c37..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandler.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONTAINER_NAME; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.DATABASE_NAME; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.ENDPOINT; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.KEY; -import static com.bytechef.component.definition.ComponentDsl.authorization; -import static com.bytechef.component.definition.ComponentDsl.component; -import static com.bytechef.component.definition.ComponentDsl.connection; -import static com.bytechef.component.definition.ComponentDsl.string; - -import com.bytechef.component.ComponentHandler; -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.action.CosmosDbChatMemoryAddMessagesAction; -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.action.CosmosDbChatMemoryDeleteAction; -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.action.CosmosDbChatMemoryGetMessagesAction; -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.action.CosmosDbChatMemoryListConversationsAction; -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.cluster.CosmosDbChatMemory; -import com.bytechef.component.definition.Authorization.AuthorizationType; -import com.bytechef.component.definition.ComponentCategory; -import com.bytechef.component.definition.ComponentDefinition; -import com.bytechef.component.definition.ComponentDsl.ModifiableConnectionDefinition; -import com.bytechef.component.definition.Property.ControlType; -import com.google.auto.service.AutoService; - -/** - * @author Ivica Cardic - */ -@AutoService(ComponentHandler.class) -public class CosmosDbChatMemoryComponentHandler implements ComponentHandler { - - private static final ModifiableConnectionDefinition CONNECTION_DEFINITION = connection() - .properties( - string(ENDPOINT) - .label("Endpoint") - .description("The Azure Cosmos DB account endpoint URI.") - .required(true), - string(DATABASE_NAME) - .label("Database Name") - .description("The database name.") - .defaultValue("spring_ai") - .required(false), - string(CONTAINER_NAME) - .label("Container Name") - .description("The container name for storing chat memory.") - .defaultValue("chat_memory") - .required(false)) - .authorizations( - authorization(AuthorizationType.CUSTOM) - .properties( - string(KEY) - .label("Key") - .description("The Azure Cosmos DB account key.") - .controlType(ControlType.PASSWORD) - .required(true))); - - private static final ComponentDefinition COMPONENT_DEFINITION = component("cosmosDbChatMemory") - .title("Cosmos DB Chat Memory") - .description( - "Cosmos DB Chat Memory stores conversation history in Azure Cosmos DB for globally distributed, " + - "scalable persistent storage.") - .icon("path:assets/cosmosdb-chat-memory.svg") - .categories(ComponentCategory.ARTIFICIAL_INTELLIGENCE) - .connection(CONNECTION_DEFINITION) - .actions( - CosmosDbChatMemoryAddMessagesAction.ACTION_DEFINITION, - CosmosDbChatMemoryGetMessagesAction.ACTION_DEFINITION, - CosmosDbChatMemoryDeleteAction.ACTION_DEFINITION, - CosmosDbChatMemoryListConversationsAction.ACTION_DEFINITION) - .clusterElements(CosmosDbChatMemory.CLUSTER_ELEMENT_DEFINITION); - - @Override - public ComponentDefinition getDefinition() { - return COMPONENT_DEFINITION; - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryAddMessagesAction.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryAddMessagesAction.java deleted file mode 100644 index 321fdadcf9c..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryAddMessagesAction.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.action; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONVERSATION_ID; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.MESSAGES; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.MESSAGE_CONTENT; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.MESSAGE_ROLE; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils.getChatMemoryRepository; -import static com.bytechef.component.definition.ComponentDsl.action; -import static com.bytechef.component.definition.ComponentDsl.array; -import static com.bytechef.component.definition.ComponentDsl.object; -import static com.bytechef.component.definition.ComponentDsl.option; -import static com.bytechef.component.definition.ComponentDsl.string; - -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils; -import com.bytechef.component.definition.ActionContext; -import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition; -import com.bytechef.component.definition.Parameters; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.UserMessage; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryAddMessagesAction { - - public static final ModifiableActionDefinition ACTION_DEFINITION = action("addMessages") - .title("Add Messages") - .description("Adds messages to the chat memory for a conversation.") - .properties( - string(CONVERSATION_ID) - .label("Conversation ID") - .description("The unique identifier for the conversation.") - .options(CosmosDbChatMemoryUtils.getFirstMessages()) - .required(true), - array(MESSAGES) - .label("Messages") - .description("The messages to add to the conversation.") - .required(true) - .items( - object() - .properties( - string(MESSAGE_ROLE) - .label("Role") - .description("The role of the message sender.") - .required(true) - .options( - option("User", "user"), - option("Assistant", "assistant")), - string(MESSAGE_CONTENT) - .label("Content") - .description("The content of the message.") - .required(true)))) - .perform(CosmosDbChatMemoryAddMessagesAction::perform); - - private CosmosDbChatMemoryAddMessagesAction() { - } - - protected static Object perform( - Parameters inputParameters, Parameters connectionParameters, ActionContext context) { - - String conversationId = inputParameters.getRequiredString(CONVERSATION_ID); - Object[] messagesArray = inputParameters.getRequiredArray(MESSAGES); - - ChatMemoryRepository repository = getChatMemoryRepository(connectionParameters); - List existingMessages = new ArrayList<>(repository.findByConversationId(conversationId)); - - for (Object messageObj : messagesArray) { - if (messageObj instanceof Map messageMap) { - String role = (String) messageMap.get(MESSAGE_ROLE); - String content = (String) messageMap.get(MESSAGE_CONTENT); - Message message = createMessage(role, content); - - existingMessages.add(message); - } - } - - repository.saveAll(conversationId, existingMessages); - - return Map.of( - "conversationId", conversationId, - "messageCount", existingMessages.size()); - } - - private static Message createMessage(String role, String content) { - return switch (role) { - case "user" -> new UserMessage(content); - case "assistant" -> new AssistantMessage(content); - default -> throw new IllegalArgumentException("Unsupported role: " + role); - }; - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryDeleteAction.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryDeleteAction.java deleted file mode 100644 index 1b3aecefae3..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryDeleteAction.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.action; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONVERSATION_ID; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils.getChatMemoryRepository; -import static com.bytechef.component.definition.ComponentDsl.action; -import static com.bytechef.component.definition.ComponentDsl.bool; -import static com.bytechef.component.definition.ComponentDsl.object; -import static com.bytechef.component.definition.ComponentDsl.outputSchema; -import static com.bytechef.component.definition.ComponentDsl.string; - -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils; -import com.bytechef.component.definition.ActionContext; -import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition; -import com.bytechef.component.definition.Parameters; -import java.util.Map; -import org.springframework.ai.chat.memory.ChatMemoryRepository; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryDeleteAction { - - public static final ModifiableActionDefinition ACTION_DEFINITION = action("deleteConversation") - .title("Delete Conversation") - .description("Deletes all messages for a conversation.") - .properties( - string(CONVERSATION_ID) - .label("Conversation ID") - .description("The unique identifier for the conversation to delete.") - .options(CosmosDbChatMemoryUtils.getFirstMessages()) - .required(true)) - .output( - outputSchema( - object() - .properties( - string(CONVERSATION_ID), - bool("deleted")))) - .perform(CosmosDbChatMemoryDeleteAction::perform); - - private CosmosDbChatMemoryDeleteAction() { - } - - protected static Object perform( - Parameters inputParameters, Parameters connectionParameters, ActionContext context) { - - String conversationId = inputParameters.getRequiredString(CONVERSATION_ID); - - ChatMemoryRepository repository = getChatMemoryRepository(connectionParameters); - - repository.deleteByConversationId(conversationId); - - return Map.of( - CONVERSATION_ID, conversationId, - "deleted", true); - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryGetMessagesAction.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryGetMessagesAction.java deleted file mode 100644 index 062bf306042..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryGetMessagesAction.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.action; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONVERSATION_ID; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils.getChatMemoryRepository; -import static com.bytechef.component.definition.ComponentDsl.action; -import static com.bytechef.component.definition.ComponentDsl.array; -import static com.bytechef.component.definition.ComponentDsl.object; -import static com.bytechef.component.definition.ComponentDsl.outputSchema; -import static com.bytechef.component.definition.ComponentDsl.string; - -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils; -import com.bytechef.component.definition.ActionContext; -import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition; -import com.bytechef.component.definition.Parameters; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.MessageType; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryGetMessagesAction { - - public static final ModifiableActionDefinition ACTION_DEFINITION = action("getMessages") - .title("Get Messages") - .description("Retrieves all messages from a conversation.") - .properties( - string(CONVERSATION_ID) - .label("Conversation ID") - .description("The unique identifier for the conversation.") - .options(CosmosDbChatMemoryUtils.getFirstMessages()) - .required(true)) - .output( - outputSchema( - object() - .properties( - string(CONVERSATION_ID), - array("messages") - .items( - object() - .properties( - string("role"), - string("content")))))) - .perform(CosmosDbChatMemoryGetMessagesAction::perform); - - private CosmosDbChatMemoryGetMessagesAction() { - } - - protected static Object perform( - Parameters inputParameters, Parameters connectionParameters, ActionContext context) { - - String conversationId = inputParameters.getRequiredString(CONVERSATION_ID); - - ChatMemoryRepository repository = getChatMemoryRepository(connectionParameters); - List messages = repository.findByConversationId(conversationId); - - List> messageList = messages.stream() - .map(CosmosDbChatMemoryGetMessagesAction::toMessageMap) - .toList(); - - return Map.of( - CONVERSATION_ID, conversationId, - "messages", messageList); - } - - private static Map toMessageMap(Message message) { - Map map = new HashMap<>(); - - MessageType messageType = message.getMessageType(); - - if (messageType == MessageType.USER) { - map.put("role", "user"); - } else if (messageType == MessageType.ASSISTANT) { - map.put("role", "assistant"); - } else if (messageType == MessageType.SYSTEM) { - map.put("role", "system"); - } else { - map.put("role", messageType.getValue()); - } - - map.put("content", message.getText()); - - return map; - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryListConversationsAction.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryListConversationsAction.java deleted file mode 100644 index ea85814070b..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/action/CosmosDbChatMemoryListConversationsAction.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.action; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils.getChatMemoryRepository; -import static com.bytechef.component.definition.ComponentDsl.action; -import static com.bytechef.component.definition.ComponentDsl.array; -import static com.bytechef.component.definition.ComponentDsl.integer; -import static com.bytechef.component.definition.ComponentDsl.object; -import static com.bytechef.component.definition.ComponentDsl.outputSchema; -import static com.bytechef.component.definition.ComponentDsl.string; - -import com.bytechef.component.definition.ActionContext; -import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition; -import com.bytechef.component.definition.Parameters; -import java.util.List; -import java.util.Map; -import org.springframework.ai.chat.memory.ChatMemoryRepository; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryListConversationsAction { - - public static final ModifiableActionDefinition ACTION_DEFINITION = action("listConversations") - .title("List Conversations") - .description("Lists all conversation IDs in the chat memory.") - .output( - outputSchema( - object() - .properties( - array("conversationIds") - .items(string()), - integer("count")))) - .perform(CosmosDbChatMemoryListConversationsAction::perform); - - private CosmosDbChatMemoryListConversationsAction() { - } - - protected static Object perform( - Parameters inputParameters, Parameters connectionParameters, ActionContext context) { - - ChatMemoryRepository repository = getChatMemoryRepository(connectionParameters); - List conversationIds = repository.findConversationIds(); - - return Map.of( - "conversationIds", conversationIds, - "count", conversationIds.size()); - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/cluster/CosmosDbChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/cluster/CosmosDbChatMemory.java deleted file mode 100644 index 00724e9d01d..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/cluster/CosmosDbChatMemory.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.cluster; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONVERSATION_ID; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils.getChatMemoryRepository; -import static com.bytechef.component.definition.ComponentDsl.string; -import static com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction.CHAT_MEMORY; - -import com.bytechef.component.ai.agent.chat.memory.cosmosdb.util.CosmosDbChatMemoryUtils; -import com.bytechef.component.definition.ClusterElementDefinition; -import com.bytechef.component.definition.ComponentDsl; -import com.bytechef.component.definition.Parameters; -import com.bytechef.platform.component.ComponentConnection; -import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; -import java.util.Map; -import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.api.BaseAdvisor; -import org.springframework.ai.chat.memory.MessageWindowChatMemory; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemory { - - private CosmosDbChatMemory() { - } - - public static final ClusterElementDefinition CLUSTER_ELEMENT_DEFINITION = - ComponentDsl.clusterElement("chatMemory") - .title("Cosmos DB Chat Memory") - .description("Memory is retrieved from Azure Cosmos DB and added as prior messages in the conversation.") - .properties( - string(CONVERSATION_ID) - .label("Conversation ID") - .description("The unique identifier for the conversation.") - .options(CosmosDbChatMemoryUtils.getFirstMessages()) - .required(true)) - .type(CHAT_MEMORY) - .object(() -> CosmosDbChatMemory::apply); - - protected static ChatMemoryFunction.Result apply( - Parameters inputParameters, Parameters connectionParameters, Parameters extensions, - Map componentConnections) { - - MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() - .chatMemoryRepository(getChatMemoryRepository(connectionParameters)) - .build(); - - return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) - .build(), - chatMemory); - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/constant/CosmosDbChatMemoryConstants.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/constant/CosmosDbChatMemoryConstants.java deleted file mode 100644 index 0b951ddab79..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/constant/CosmosDbChatMemoryConstants.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryConstants { - - public static final String CONTAINER_NAME = "containerName"; - public static final String CONVERSATION_ID = "conversationId"; - public static final String DATABASE_NAME = "databaseName"; - public static final String ENDPOINT = "endpoint"; - public static final String KEY = "key"; - public static final String MESSAGES = "messages"; - public static final String MESSAGE_CONTENT = "content"; - public static final String MESSAGE_ROLE = "role"; -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/util/CosmosDbChatMemoryUtils.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/util/CosmosDbChatMemoryUtils.java deleted file mode 100644 index 39ffcb08f70..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/util/CosmosDbChatMemoryUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb.util; - -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.CONTAINER_NAME; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.DATABASE_NAME; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.ENDPOINT; -import static com.bytechef.component.ai.agent.chat.memory.cosmosdb.constant.CosmosDbChatMemoryConstants.KEY; -import static com.bytechef.component.definition.ComponentDsl.option; - -import com.azure.cosmos.CosmosAsyncClient; -import com.azure.cosmos.CosmosClientBuilder; -import com.bytechef.component.definition.ActionDefinition; -import com.bytechef.component.definition.ComponentDsl; -import com.bytechef.component.definition.Parameters; -import java.util.ArrayList; -import java.util.List; -import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.memory.repository.cosmosdb.CosmosDBChatMemoryRepository; -import org.springframework.ai.chat.memory.repository.cosmosdb.CosmosDBChatMemoryRepositoryConfig; -import org.springframework.ai.chat.messages.Message; - -/** - * @author Ivica Cardic - */ -public class CosmosDbChatMemoryUtils { - - private CosmosDbChatMemoryUtils() { - } - - public static ChatMemoryRepository getChatMemoryRepository(Parameters connectionParameters) { - String endpoint = connectionParameters.getRequiredString(ENDPOINT); - String key = connectionParameters.getRequiredString(KEY); - String databaseName = connectionParameters.getString(DATABASE_NAME, "spring_ai"); - String containerName = connectionParameters.getString(CONTAINER_NAME, "chat_memory"); - - CosmosAsyncClient cosmosAsyncClient = new CosmosClientBuilder() - .endpoint(endpoint) - .key(key) - .buildAsyncClient(); - - CosmosDBChatMemoryRepositoryConfig config = CosmosDBChatMemoryRepositoryConfig.builder() - .withCosmosClient(cosmosAsyncClient) - .withDatabaseName(databaseName) - .withContainerName(containerName) - .build(); - - return CosmosDBChatMemoryRepository.create(config); - } - - public static ActionDefinition.OptionsFunction getFirstMessages() { - return (inputParameters, connectionParameters, lookupDependsOnPaths, searchText, context) -> { - ChatMemoryRepository chatMemoryRepository = getChatMemoryRepository(connectionParameters); - - List> options = new ArrayList<>(); - - List conversationIds = chatMemoryRepository.findConversationIds(); - - for (String conversationId : conversationIds) { - List messages = chatMemoryRepository.findByConversationId(conversationId); - - Message message = messages.getFirst(); - - options.add(option(conversationId, conversationId, message.getText())); - } - - return options; - }; - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/resources/assets/cosmosdb-chat-memory.svg b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/resources/assets/cosmosdb-chat-memory.svg deleted file mode 100644 index a526c389283..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/main/resources/assets/cosmosdb-chat-memory.svg +++ /dev/null @@ -1 +0,0 @@ -Icon-databases-121 \ No newline at end of file diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandlerTest.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandlerTest.java deleted file mode 100644 index f47aa0138c7..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandlerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.cosmosdb; - -import com.bytechef.test.jsonasssert.JsonFileAssert; -import org.junit.jupiter.api.Test; - -public class CosmosDbChatMemoryComponentHandlerTest { - - @Test - public void testGetComponentDefinition() { - JsonFileAssert.assertEquals( - "definition/cosmos-db-chat-memory_v1.json", new CosmosDbChatMemoryComponentHandler().getDefinition()); - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/resources/definition/cosmos-db-chat-memory_v1.json b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/resources/definition/cosmos-db-chat-memory_v1.json deleted file mode 100644 index 02dbb951d57..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/resources/definition/cosmos-db-chat-memory_v1.json +++ /dev/null @@ -1,994 +0,0 @@ -{ - "actions": [ { - "batch": null, - "beforeResume": null, - "beforeSuspend": null, - "beforeTimeoutResume": null, - "deprecated": null, - "description": "Adds messages to the chat memory for a conversation.", - "help": null, - "metadata": null, - "name": "addMessages", - "outputDefinition": null, - "perform": { }, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": "The unique identifier for the conversation.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Conversation ID", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": { - "options": { }, - "optionsLookupDependsOn": null - }, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": "The messages to add to the conversation.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": "The role of the message sender.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Role", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "role", - "options": [ { - "description": null, - "label": "User", - "value": "user" - }, { - "description": null, - "label": "Assistant", - "value": "assistant" - } ], - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": "The content of the message.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Content", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "content", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "required": null, - "type": "OBJECT" - } ], - "label": "Messages", - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "messages", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": true, - "type": "ARRAY" - } ], - "resumePerform": null, - "title": "Add Messages", - "workflowNodeDescription": null - }, { - "batch": null, - "beforeResume": null, - "beforeSuspend": null, - "beforeTimeoutResume": null, - "deprecated": null, - "description": "Retrieves all messages from a conversation.", - "help": null, - "metadata": null, - "name": "getMessages", - "outputDefinition": { - "output": null, - "outputResponse": { - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "role", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "content", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - } ], - "required": null, - "type": "OBJECT" - } ], - "label": null, - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "messages", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "ARRAY" - } ], - "required": null, - "type": "OBJECT" - }, - "placeholder": null, - "sampleOutput": null - }, - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "role", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "content", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - } ], - "required": null, - "type": "OBJECT" - } ], - "label": null, - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "messages", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "ARRAY" - } ], - "required": null, - "type": "OBJECT" - }, - "sampleOutput": null - }, - "perform": { }, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": "The unique identifier for the conversation.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Conversation ID", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": { - "options": { }, - "optionsLookupDependsOn": null - }, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "resumePerform": null, - "title": "Get Messages", - "workflowNodeDescription": null - }, { - "batch": null, - "beforeResume": null, - "beforeSuspend": null, - "beforeTimeoutResume": null, - "deprecated": null, - "description": "Deletes all messages for a conversation.", - "help": null, - "metadata": null, - "name": "deleteConversation", - "outputDefinition": { - "output": null, - "outputResponse": { - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "name": "deleted", - "options": [ { - "description": null, - "label": "True", - "value": true - }, { - "description": null, - "label": "False", - "value": false - } ], - "placeholder": null, - "required": null, - "type": "BOOLEAN" - } ], - "required": null, - "type": "OBJECT" - }, - "placeholder": null, - "sampleOutput": null - }, - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "name": "deleted", - "options": [ { - "description": null, - "label": "True", - "value": true - }, { - "description": null, - "label": "False", - "value": false - } ], - "placeholder": null, - "required": null, - "type": "BOOLEAN" - } ], - "required": null, - "type": "OBJECT" - }, - "sampleOutput": null - }, - "perform": { }, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": "The unique identifier for the conversation to delete.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Conversation ID", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": { - "options": { }, - "optionsLookupDependsOn": null - }, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "resumePerform": null, - "title": "Delete Conversation", - "workflowNodeDescription": null - }, { - "batch": null, - "beforeResume": null, - "beforeSuspend": null, - "beforeTimeoutResume": null, - "deprecated": null, - "description": "Lists all conversation IDs in the chat memory.", - "help": null, - "metadata": null, - "name": "listConversations", - "outputDefinition": { - "output": null, - "outputResponse": { - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": null, - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - } ], - "label": null, - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "conversationIds", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "ARRAY" - }, { - "advancedOption": null, - "controlType": "INTEGER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "maxValue": null, - "metadata": { }, - "minValue": null, - "name": "count", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "INTEGER" - } ], - "required": null, - "type": "OBJECT" - }, - "placeholder": null, - "sampleOutput": null - }, - "outputSchema": { - "additionalProperties": null, - "advancedOption": null, - "controlType": "OBJECT_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "metadata": { }, - "multipleValues": null, - "name": null, - "options": null, - "optionsDataSource": null, - "placeholder": null, - "properties": [ { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": null, - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": null, - "type": "STRING" - } ], - "label": null, - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "conversationIds", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "ARRAY" - }, { - "advancedOption": null, - "controlType": "INTEGER", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": null, - "maxValue": null, - "metadata": { }, - "minValue": null, - "name": "count", - "options": null, - "optionsDataSource": null, - "placeholder": null, - "required": null, - "type": "INTEGER" - } ], - "required": null, - "type": "OBJECT" - }, - "sampleOutput": null - }, - "perform": { }, - "processErrorResponse": null, - "properties": null, - "resumePerform": null, - "title": "List Conversations", - "workflowNodeDescription": null - } ], - "clusterElements": [ { - "name": "chatMemory", - "description": "Memory is retrieved from Azure Cosmos DB and added as prior messages in the conversation.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": "The unique identifier for the conversation.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Conversation ID", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "conversationId", - "options": null, - "optionsDataSource": { - "options": { }, - "optionsLookupDependsOn": null - }, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "title": "Cosmos DB Chat Memory", - "type": { - "name": "CHAT_MEMORY", - "key": "chatMemory", - "label": "Memory", - "multipleElements": false, - "required": false - }, - "workflowNodeDescription": null - } ], - "componentCategories": [ { - "name": "artificial-intelligence", - "label": "Artificial Intelligence" - } ], - "connection": { - "authorizationRequired": null, - "authorizations": [ { - "acquire": null, - "apply": null, - "authorizationCallback": null, - "authorizationUrl": null, - "clientId": null, - "clientSecret": null, - "description": null, - "detectOn": null, - "name": "custom", - "oauth2AuthorizationExtraQueryParameters": null, - "pkce": null, - "properties": [ { - "advancedOption": null, - "controlType": "PASSWORD", - "defaultValue": null, - "description": "The Azure Cosmos DB account key.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Key", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "key", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "refresh": null, - "refreshOn": null, - "refreshToken": null, - "refreshUrl": null, - "scopes": null, - "title": null, - "tokenUrl": null, - "type": "CUSTOM" - } ], - "baseUri": null, - "help": null, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": null, - "description": "The Azure Cosmos DB account endpoint URI.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Endpoint", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "endpoint", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": "spring_ai", - "description": "The database name.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Database Name", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "databaseName", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": false, - "type": "STRING" - }, { - "advancedOption": null, - "controlType": "TEXT", - "defaultValue": "chat_memory", - "description": "The container name for storing chat memory.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Container Name", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "containerName", - "options": null, - "optionsDataSource": null, - "optionsLoadedDynamically": null, - "placeholder": null, - "regex": null, - "required": false, - "type": "STRING" - } ], - "test": null, - "version": 1 - }, - "customAction": null, - "customActionHelp": null, - "description": "Cosmos DB Chat Memory stores conversation history in Azure Cosmos DB for globally distributed, scalable persistent storage.", - "icon": "path:assets/cosmosdb-chat-memory.svg", - "metadata": null, - "name": "cosmosDbChatMemory", - "resources": null, - "tags": null, - "title": "Cosmos DB Chat Memory", - "triggers": null, - "unifiedApi": null, - "version": 1 -} \ No newline at end of file diff --git a/server/libs/platform/platform-knowledge-base/platform-knowledge-base-worker/build.gradle.kts b/server/libs/platform/platform-knowledge-base/platform-knowledge-base-worker/build.gradle.kts index 5e15d6c2e67..33b8f783d59 100644 --- a/server/libs/platform/platform-knowledge-base/platform-knowledge-base-worker/build.gradle.kts +++ b/server/libs/platform/platform-knowledge-base/platform-knowledge-base-worker/build.gradle.kts @@ -1,6 +1,7 @@ dependencies { implementation("com.knuddels:jtokkit") implementation("org.springframework:spring-context") + implementation("org.springframework.boot:spring-boot-autoconfigure") implementation("org.springframework.ai:spring-ai-mistral-ai") implementation("org.springframework.ai:spring-ai-markdown-document-reader") implementation("org.springframework.ai:spring-ai-openai") diff --git a/settings.gradle.kts b/settings.gradle.kts index e77b0569171..74be3e36a34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -230,7 +230,6 @@ include("server:libs:modules:components:ahrefs") include("server:libs:modules:components:ai:agent") include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-builtin") include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-cassandra") -include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-cosmosdb") include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-in-memory") include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-jdbc") include("server:libs:modules:components:ai:agent:chat-memory:chat-memory-mongodb") From a798a98d5263d47d18d0c4eb2b1f9c55e8e2478a Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 18:20:45 +0200 Subject: [PATCH 08/24] 1652 Migrate AI agent module from deprecated toolCallbacks to Spring AI M7 ToolSpec API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring AI 2.0.0-M7 deprecated and marked for removal both ChatClient.ChatClientRequestSpec.toolCallbacks(List) and ChatClient.ChatClientRequestSpec.toolContext(Map) in favor of a unified tools(Consumer) entry point (see PR #6085 "Introduce ToolSpec fluent API"). This branch has a single deprecated call site — the toolCallbacks() registration in AbstractAiAgentChatAction — which is replaced with the new tools(spec -> spec.callbacks(...)) form. Behavior is identical; the new API just consolidates tool registration into a single fluent surface. (The toolContext() migrations from the upstream change live in suspend/resume code that is not part of this branch.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../component/ai/agent/action/AbstractAiAgentChatAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index ca330c90d7d..ab8f9093e97 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -152,10 +152,10 @@ protected ChatClient.ChatClientRequestSpec getChatClientRequestSpec( .advisors(getAdvisors(clusterElementMap, connectionParameters, context)) .advisors(getConversationAdvisor(conversationId)) .messages(ModelUtils.getMessages(inputParameters, context)) - .toolCallbacks( + .tools(spec -> spec.callbacks( getToolCallbacks( clusterElementMap.getClusterElements(BaseToolFunction.TOOLS), connectionParameters, - context.isEditorEnvironment(), toolExecutionListener, toolSimulations, chatModel, context)); + context.isEditorEnvironment(), toolExecutionListener, toolSimulations, chatModel, context))); } private ChatMemoryFunction.Result buildChatMemoryResult( From f02ff6c65195bd989ca28034e02001534e75af9d Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 26 May 2026 11:40:28 +0200 Subject: [PATCH 09/24] 1652 Fix Spring AI 2.0.0-M7 boot failure and update MCP SDK fork to 2.0.0-M3 Two related fixes for the recently bumped Spring AI 2.0.0-M7: 1) Spring AI M7 packaging oversight. The M7 spring-ai-starter-mcp-client POM no longer depends on spring-ai-autoconfigure-mcp-client-common, but spring-ai-autoconfigure-mcp-client-httpclient:2.0.0-M7 still references classes in org.springframework.ai.mcp.client.common. autoconfigure.* (McpSseClientProperties, NamedClientMcpTransport, etc.). Boot fails at the bean-definition phase with ClassNotFoundException for McpSseClientProperties. The M7 jar exists on Maven Central (just not listed in the directory index) so an explicit dependency on spring-ai-autoconfigure-mcp-client-common pulls it back in. Added with a comment explaining the workaround so it can be removed once a future Spring AI release re-includes the transitive dependency. 2) MCP SDK 2.0.0-M3 sync. The version pin in libs.versions.toml was still M2 even though Spring AI M7 already resolves mcp-core to M3 transitively. Bumped to M3 explicitly to remove the mixed-version state, and updated the forked FilterableMcpAsyncServer.java to match the upstream M3 changes: - DefaultMcpStreamableServerSessionFactory constructor now takes a trailing jsonSchemaValidator parameter - addTool() validates the tool's inputSchema / outputSchema via jsonSchemaValidator.assertConforms() before insertion - McpSchema.TextContent / ListToolsResult / ListResourcesResult / ListResourceTemplatesResult / ListPromptsResult constructors are replaced with their builder().build() forms (the constructors are no longer public in M3) - Javadoc header bumped from "fork of McpAsyncServer (2.0.0-M2)" to "(2.0.0-M3)" Verified with bootRun: ServerApplication now starts cleanly in ~19s. Co-Authored-By: Claude Opus 4.7 (1M context) --- gradle/libs.versions.toml | 2 +- server/apps/server-app/build.gradle.kts | 1 + .../mcp/server/FilterableMcpAsyncServer.java | 36 ++++++++++++++----- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d05ce64654..570df95e62b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ checkstyle = "13.4.0" com-google-auto-service = "1.1.1" findsecbugs = "1.14.0" graalvm = "25.0.2" -io-modelcontextprotocol-sdk = "2.0.0-M2" +io-modelcontextprotocol-sdk = "2.0.0-M3" jackson = "2.19.2" jacoco = "0.8.13" java = "25" diff --git a/server/apps/server-app/build.gradle.kts b/server/apps/server-app/build.gradle.kts index 363a339f9cb..11388bae90a 100644 --- a/server/apps/server-app/build.gradle.kts +++ b/server/apps/server-app/build.gradle.kts @@ -80,6 +80,7 @@ dependencies { implementation("io.awspring.cloud:spring-cloud-aws-starter-sqs") implementation(libs.org.springdoc.springdoc.openapi.starter.common) implementation(libs.org.springdoc.springdoc.openapi.starter.webmvc.ui) + implementation("org.springframework.ai:spring-ai-autoconfigure-mcp-client-common") implementation("org.springframework.ai:spring-ai-starter-mcp-client") implementation("org.springframework.ai:spring-ai-starter-model-anthropic") implementation("org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc") diff --git a/server/libs/platform/platform-mcp/platform-mcp-server-support/src/main/java/com/bytechef/platform/mcp/server/FilterableMcpAsyncServer.java b/server/libs/platform/platform-mcp/platform-mcp-server-support/src/main/java/com/bytechef/platform/mcp/server/FilterableMcpAsyncServer.java index 7280caea057..6782d32bb47 100644 --- a/server/libs/platform/platform-mcp/platform-mcp-server-support/src/main/java/com/bytechef/platform/mcp/server/FilterableMcpAsyncServer.java +++ b/server/libs/platform/platform-mcp/platform-mcp-server-support/src/main/java/com/bytechef/platform/mcp/server/FilterableMcpAsyncServer.java @@ -61,7 +61,7 @@ import reactor.core.publisher.Mono; /** - * A fork of the MCP SDK's {@code McpAsyncServer} (2.0.0-M2) that adds per-session tool filtering. + * A fork of the MCP SDK's {@code McpAsyncServer} (2.0.0-M3) that adds per-session tool filtering. * *

* The standard {@code McpAsyncServer} returns all registered tools to every client session. This class introduces a @@ -204,7 +204,7 @@ public class FilterableMcpAsyncServer { mcpTransportProvider.setSessionFactory(new DefaultMcpStreamableServerSessionFactory(requestTimeout, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers, - sessionId -> this.cleanupForSession(sessionId))); + sessionId -> this.cleanupForSession(sessionId), this.jsonSchemaValidator)); } private Map prepareNotificationHandlers( @@ -394,6 +394,17 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica return Mono.error(new IllegalStateException("Server must be configured with tool capabilities")); } + try { + var tool = toolSpecification.tool(); + + this.jsonSchemaValidator.assertConforms( + "Tool '" + tool.name() + "' inputSchema", tool.inputSchema()); + this.jsonSchemaValidator.assertConforms( + "Tool '" + tool.name() + "' outputSchema", tool.outputSchema()); + } catch (IllegalArgumentException exception) { + return Mono.error(exception); + } + var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification); return Mono.defer(() -> { @@ -475,7 +486,8 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal log.warn(content); return CallToolResult.builder() - .content(List.of(new McpSchema.TextContent(content))) + .content(List.of(McpSchema.TextContent.builder(content) + .build())) .isError(true) .build(); } @@ -487,14 +499,16 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal log.warn("Tool call result validation failed: {}", validation.errorMessage()); return CallToolResult.builder() - .content(List.of(new McpSchema.TextContent(validation.errorMessage()))) + .content(List.of(McpSchema.TextContent.builder(validation.errorMessage()) + .build())) .isError(true) .build(); } if (Utils.isEmpty(result.content())) { return CallToolResult.builder() - .content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput()))) + .content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()) + .build())) .isError(result.isError()) .structuredContent(result.structuredContent()) .build(); @@ -605,7 +619,8 @@ private McpRequestHandler toolsListRequestHandler() { .map(McpServerFeatures.AsyncToolSpecification::tool) .toList(); - return Mono.just(new McpSchema.ListToolsResult(toolList, null)); + return Mono.just(McpSchema.ListToolsResult.builder(toolList) + .build()); }; } // --- end ByteChef modification --- @@ -858,7 +873,8 @@ private McpRequestHandler resourcesListRequestHan .map(McpServerFeatures.AsyncResourceSpecification::resource) .toList(); - return Mono.just(new McpSchema.ListResourcesResult(resourceList, null)); + return Mono.just(McpSchema.ListResourcesResult.builder(resourceList) + .build()); }; } @@ -869,7 +885,8 @@ private McpRequestHandler resourceTemplat .map(McpServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate) .toList(); - return Mono.just(new McpSchema.ListResourceTemplatesResult(resourceList, null)); + return Mono.just(McpSchema.ListResourceTemplatesResult.builder(resourceList) + .build()); }; } @@ -1061,7 +1078,8 @@ private McpRequestHandler promptsListRequestHandler .map(McpServerFeatures.AsyncPromptSpecification::prompt) .toList(); - return Mono.just(new McpSchema.ListPromptsResult(promptList, null)); + return Mono.just(McpSchema.ListPromptsResult.builder(promptList) + .build()); }; } From dabfe7d211673dc9efc9838b1131fc6c3eadf2ab Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Sun, 31 May 2026 10:16:15 +0200 Subject: [PATCH 10/24] 1652 Upgrade Spring AI from 2.0.0-M7 to 2.0.0-M8 M8 is a small stabilization release (4 bug fixes, 4 minor features); no API breakage, no module coordinate changes. Vendored spring-ai-tool-search-tool fork's super(...) patch remains valid since ToolCallAdvisor signature is unchanged in M8. Co-Authored-By: Claude Opus 4.7 (1M context) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 570df95e62b..7d595573c94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ org-mapstruct-extensions-spring = "2.0.0" org-springdoc = "3.0.3" pmd = "7.23.0" spotbugs = "4.9.8" -spring-ai = "2.0.0-M7" +spring-ai = "2.0.0-M8" spring-boot = "4.0.6" spring-cloud-aws = "4.0.2" spring-cloud-dependencies = "2025.1.1" From 517ffc3d6dde0e948bb1f67d77265b63b0860a2c Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Wed, 27 May 2026 21:07:22 +0200 Subject: [PATCH 11/24] 1652 Drop unused PostgreSQLContainerConfiguration import from WorkerApplicationIntTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit worker-app has no JDBC dependency — it's stateless and reaches database state through *-remote-client modules. The vestigial @Import predates the remote-client refactor. Under Spring Boot 4, @ServiceConnection now throws when no matching ConnectionDetails registrar is on the classpath, so importing a Postgres container in an app without JDBC autoconfig fails the context load. Removing the unused import restores green testIntegration without affecting sibling app tests (connection-app, execution-app, etc.) that legitimately pull in JDBC starters. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/bytechef/worker/WorkerApplicationIntTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/ee/apps/worker-app/src/test/java/com/bytechef/worker/WorkerApplicationIntTest.java b/server/ee/apps/worker-app/src/test/java/com/bytechef/worker/WorkerApplicationIntTest.java index 48054d3b114..b68bdb66cee 100644 --- a/server/ee/apps/worker-app/src/test/java/com/bytechef/worker/WorkerApplicationIntTest.java +++ b/server/ee/apps/worker-app/src/test/java/com/bytechef/worker/WorkerApplicationIntTest.java @@ -7,10 +7,8 @@ package com.bytechef.worker; -import com.bytechef.test.config.testcontainers.PostgreSQLContainerConfiguration; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; /** * @version ee @@ -18,7 +16,6 @@ * @author Ivica Cardic */ @SpringBootTest(classes = WorkerApplication.class) -@Import(PostgreSQLContainerConfiguration.class) public class WorkerApplicationIntTest { @Test From 1e56a2930c9a02ab17d1525453f0bdf053b4b5d5 Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Wed, 27 May 2026 21:20:30 +0200 Subject: [PATCH 12/24] 1652 Regenerate Gemini/Mistral/OpenAI component snapshots for Spring AI M8 Snapshots drifted because the LLM components pull model lists dynamically from Spring AI's provider enums (GoogleGenAiChatModel.ChatModel.values(), MistralAiApi.ChatModel.values(), OpenAiApi.ChatModel.values()), and M8 refreshed those enums: - Gemini: dropped -preview suffix on flash-lite, added gemini-3.5-flash - OpenAI: gained ~3 new chat models in one action - Mistral: trimmed one retired model The bulk of the diff is whitespace from Jackson 3.x's default pretty-printer emitting " : " (with leading space) instead of ": "; net model changes are small. Other LLM components (anthropic, ollama, azure-open-ai, amazon-bedrock, stability) verified unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../test/resources/definition/gemini_v1.json | 24 +++++++++---------- .../test/resources/definition/mistral_v1.json | 8 ------- .../test/resources/definition/open-ai_v1.json | 12 ++++++++++ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/server/libs/modules/components/ai/llm/gemini/src/test/resources/definition/gemini_v1.json b/server/libs/modules/components/ai/llm/gemini/src/test/resources/definition/gemini_v1.json index 5e185916bc3..677a5a06407 100644 --- a/server/libs/modules/components/ai/llm/gemini/src/test/resources/definition/gemini_v1.json +++ b/server/libs/modules/components/ai/llm/gemini/src/test/resources/definition/gemini_v1.json @@ -54,16 +54,16 @@ "value": "gemini-2.5-pro" }, { "description": null, - "label": "gemini-3-flash-preview", - "value": "gemini-3-flash-preview" - }, { - "description": null, - "label": "gemini-3.1-flash-lite-preview", - "value": "gemini-3.1-flash-lite-preview" + "label": "gemini-3.1-flash-lite", + "value": "gemini-3.1-flash-lite" }, { "description": null, "label": "gemini-3.1-pro-preview", "value": "gemini-3.1-pro-preview" + }, { + "description": null, + "label": "gemini-3.5-flash", + "value": "gemini-3.5-flash" } ], "optionsDataSource": null, "optionsLoadedDynamically": null, @@ -757,16 +757,16 @@ "value": "gemini-2.5-pro" }, { "description": null, - "label": "gemini-3-flash-preview", - "value": "gemini-3-flash-preview" - }, { - "description": null, - "label": "gemini-3.1-flash-lite-preview", - "value": "gemini-3.1-flash-lite-preview" + "label": "gemini-3.1-flash-lite", + "value": "gemini-3.1-flash-lite" }, { "description": null, "label": "gemini-3.1-pro-preview", "value": "gemini-3.1-pro-preview" + }, { + "description": null, + "label": "gemini-3.5-flash", + "value": "gemini-3.5-flash" } ], "optionsDataSource": null, "optionsLoadedDynamically": null, diff --git a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json index c1664050057..b71bed8f8a5 100644 --- a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json +++ b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json @@ -51,10 +51,6 @@ "description": null, "label": "magistral-medium-latest", "value": "magistral-medium-latest" - }, { - "description": null, - "label": "magistral-small-latest", - "value": "magistral-small-latest" }, { "description": null, "label": "ministral-14b-latest", @@ -2442,10 +2438,6 @@ "description": null, "label": "magistral-medium-latest", "value": "magistral-medium-latest" - }, { - "description": null, - "label": "magistral-small-latest", - "value": "magistral-small-latest" }, { "description": null, "label": "ministral-14b-latest", diff --git a/server/libs/modules/components/ai/llm/open-ai/src/test/resources/definition/open-ai_v1.json b/server/libs/modules/components/ai/llm/open-ai/src/test/resources/definition/open-ai_v1.json index fb749650375..5f88634dc7c 100644 --- a/server/libs/modules/components/ai/llm/open-ai/src/test/resources/definition/open-ai_v1.json +++ b/server/libs/modules/components/ai/llm/open-ai/src/test/resources/definition/open-ai_v1.json @@ -2594,6 +2594,10 @@ "minLength": null, "name": "model", "options": [ { + "description": null, + "label": "chatgpt-image-latest", + "value": "chatgpt-image-latest" + }, { "description": null, "label": "dall-e-2", "value": "dall-e-2" @@ -2613,6 +2617,14 @@ "description": null, "label": "gpt-image-1-mini", "value": "gpt-image-1-mini" + }, { + "description": null, + "label": "gpt-image-2", + "value": "gpt-image-2" + }, { + "description": null, + "label": "gpt-image-2-2026-04-21", + "value": "gpt-image-2-2026-04-21" } ], "optionsDataSource": null, "optionsLoadedDynamically": null, From 3e325c8b3f22a829829994df782540ae78315faf Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Wed, 27 May 2026 21:20:30 +0200 Subject: [PATCH 13/24] 1652 Regenerate Gemini/Mistral/OpenAI component snapshots for Spring AI M8 Snapshots drifted because the LLM components pull model lists dynamically from Spring AI's provider enums (GoogleGenAiChatModel.ChatModel.values(), MistralAiApi.ChatModel.values(), OpenAiApi.ChatModel.values()), and M8 refreshed those enums: - Gemini: dropped -preview suffix on flash-lite, added gemini-3.5-flash - OpenAI: gained ~3 new chat models in one action - Mistral: trimmed one retired model The bulk of the diff is whitespace from Jackson 3.x's default pretty-printer emitting " : " (with leading space) instead of ": "; net model changes are small. Other LLM components (anthropic, ollama, azure-open-ai, amazon-bedrock, stability) verified unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) mistral --- .../ai/llm/mistral/src/test/resources/definition/mistral_v1.json | 1 + 1 file changed, 1 insertion(+) diff --git a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json index b71bed8f8a5..cfeac17fda3 100644 --- a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json +++ b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json @@ -2697,6 +2697,7 @@ "customActionHelp": null, "description": "Open, efficient, helpful and trustworthy AI models through ground-breaking innovations.", "icon": "path:assets/mistral.svg", + "inputs": null, "metadata": null, "name": "mistral", "resources": null, From 18daea6f3478ef7ba05e1c7db45828755b715c42 Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Wed, 3 Jun 2026 17:48:16 +0200 Subject: [PATCH 14/24] 1652 - workaround for deleting toolCallAdvisorBuilder.disableInternalConversationHistory() --- .../chat-memory-jdbc/build.gradle.kts | 1 + .../util/OrderedJdbcChatMemoryRepository.java | 208 +++++++++++++++++- .../memory/cluster/VectorStoreChatMemory.java | 46 +++- .../action/AbstractAiAgentChatAction.java | 18 +- .../action/AbstractAiAgentChatActionTest.java | 69 ++++-- 5 files changed, 310 insertions(+), 32 deletions(-) diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts index e115c614be8..a6034ce2506 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts @@ -1,3 +1,4 @@ dependencies { + implementation("com.fasterxml.jackson.core:jackson-databind") implementation("org.springframework.ai:spring-ai-model-chat-memory-repository-jdbc") } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java index 8d563a2239a..6b1420a40e3 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java @@ -16,23 +16,59 @@ package com.bytechef.component.ai.agent.chat.memory.jdbc.util; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepositoryDialect; +import org.springframework.ai.chat.memory.repository.jdbc.PostgresChatMemoryRepositoryDialect; +import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.ToolResponseMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; /** - * Wraps a {@link ChatMemoryRepository} and replaces {@link #findConversationIds()} with a query that orders results by - * the most recent message timestamp (DESC) so callers always see the most active conversations first. + * Wraps a {@link ChatMemoryRepository} and: + *

    + *
  • Replaces {@link #findConversationIds()} with a query ordered by most-recent timestamp DESC.
  • + *
  • Serializes {@link AssistantMessage#getToolCalls()} and {@link ToolResponseMessage#getResponses()} as JSON in the + * {@code content} column so that tool-call sequences survive a DB round-trip. Spring AI's built-in + * {@code JdbcChatMemoryRepository} discards this information (always stores {@code ""} for both message types).
  • + *
  • Filters out broken tool-call sequences on read, preventing 400 errors from the LLM API.
  • + *
* * @author ByteChef */ public class OrderedJdbcChatMemoryRepository implements ChatMemoryRepository { + private static final String TOOL_CALLS_JSON_PREFIX = "{\"_btc_tc\":"; + private static final String TOOL_RESPONSES_JSON_PREFIX = "{\"_btc_tr\":"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final TypeReference> TOOL_CALL_LIST_TYPE = + new TypeReference<>() {}; + private static final TypeReference> TOOL_RESPONSE_LIST_TYPE = + new TypeReference<>() {}; + private final ChatMemoryRepository delegate; private final JdbcTemplate jdbcTemplate; private final String findConversationIdsOrderedSql; + private final JdbcChatMemoryRepositoryDialect dialect; @SuppressFBWarnings("EI2") public OrderedJdbcChatMemoryRepository( @@ -41,6 +77,12 @@ public OrderedJdbcChatMemoryRepository( this.delegate = delegate; this.jdbcTemplate = jdbcTemplate; this.findConversationIdsOrderedSql = findConversationIdsOrderedSql; + + DataSource dataSource = jdbcTemplate.getDataSource(); + + this.dialect = dataSource != null + ? JdbcChatMemoryRepositoryDialect.from(dataSource) + : new PostgresChatMemoryRepositoryDialect(); } @Override @@ -50,17 +92,175 @@ public List findConversationIds() { } @Override + @SuppressFBWarnings("SQL_INJECTION_SPRING_JDBC") public List findByConversationId(String conversationId) { - return delegate.findByConversationId(conversationId); + List messages = jdbcTemplate.query( + dialect.getSelectMessagesSql(), + (rs, rowNum) -> decodeMessage(rs.getString(1), rs.getString(2)), + conversationId); + + return filterBrokenToolCallSequences(messages); } @Override + @SuppressFBWarnings("SQL_INJECTION_SPRING_JDBC") public void saveAll(String conversationId, List messages) { - delegate.saveAll(conversationId, messages); + delegate.deleteByConversationId(conversationId); + + if (messages.isEmpty()) { + return; + } + + AtomicLong sequenceId = new AtomicLong(Instant.now() + .getEpochSecond()); + + jdbcTemplate.batchUpdate(dialect.getInsertMessageSql(), new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Message message = messages.get(i); + + ps.setString(1, conversationId); + ps.setString(2, encodeContent(message)); + ps.setString(3, message.getMessageType() + .name()); + ps.setTimestamp(4, new Timestamp(sequenceId.getAndIncrement() * 1000L)); + } + + @Override + public int getBatchSize() { + return messages.size(); + } + }); } @Override public void deleteByConversationId(String conversationId) { delegate.deleteByConversationId(conversationId); } + + /** + * Filters out broken tool-call sequences that cannot be sent to an LLM. Spring AI's + * {@code JdbcChatMemoryRepository} cannot reconstruct {@code ToolResponseMessage} objects with a valid + * {@code toolCallId} — it always creates empty-responses instances. This causes 400 errors from the LLM API. + *

+ * This filter removes: + *

    + *
  • An {@link AssistantMessage} with tool calls followed by one or more empty {@link ToolResponseMessage} objects + * (broken cross-turn sequences from old storage).
  • + *
  • Orphaned empty {@link ToolResponseMessage} objects not preceded by an AssistantMessage with tool calls.
  • + *
+ * Within-turn safety: the {@link AssistantMessage} with tool calls is saved before its {@link ToolResponseMessage}, + * so mid-turn reads contain only the former and it is preserved intact. + */ + static List filterBrokenToolCallSequences(List messages) { + List result = new ArrayList<>(messages.size()); + + for (int i = 0; i < messages.size(); i++) { + Message message = messages.get(i); + + if (message instanceof AssistantMessage assistantMessage && assistantMessage.hasToolCalls()) { + int j = i + 1; + + while (j < messages.size() && messages.get(j) instanceof ToolResponseMessage toolResponse + && toolResponse.getResponses() + .isEmpty()) { + j++; + } + + if (j > i + 1) { + i = j - 1; + + continue; + } + } else if (message instanceof ToolResponseMessage toolResponse && toolResponse.getResponses() + .isEmpty()) { + continue; + } + + result.add(message); + } + + return result; + } + + static String encodeContent(Message message) { + if (message instanceof AssistantMessage assistantMessage && assistantMessage.hasToolCalls()) { + try { + return TOOL_CALLS_JSON_PREFIX + OBJECT_MAPPER.writeValueAsString(assistantMessage.getToolCalls()); + } catch (JsonProcessingException e) { + return message.getText() != null ? message.getText() : ""; + } + } + + if (message instanceof ToolResponseMessage toolResponseMessage + && !toolResponseMessage.getResponses() + .isEmpty()) { + try { + return TOOL_RESPONSES_JSON_PREFIX + + OBJECT_MAPPER.writeValueAsString(toolResponseMessage.getResponses()); + } catch (JsonProcessingException e) { + return ""; + } + } + + return message.getText() != null ? message.getText() : ""; + } + + static Message decodeMessage(@Nullable String content, String type) { + MessageType messageType; + + try { + messageType = MessageType.valueOf(type); + } catch (IllegalArgumentException e) { + return new UserMessage(content != null ? content : ""); + } + + return switch (messageType) { + case USER -> new UserMessage(content != null ? content : ""); + case SYSTEM -> new SystemMessage(content != null ? content : ""); + case ASSISTANT -> decodeAssistantMessage(content); + case TOOL -> decodeToolResponseMessage(content); + }; + } + + private static Message decodeAssistantMessage(@Nullable String content) { + if (content != null && content.startsWith(TOOL_CALLS_JSON_PREFIX)) { + try { + List toolCalls = + OBJECT_MAPPER.readValue(content.substring(TOOL_CALLS_JSON_PREFIX.length()), TOOL_CALL_LIST_TYPE); + + return AssistantMessage.builder() + .content("") + .toolCalls(toolCalls) + .build(); + } catch (JsonProcessingException e) { + return new AssistantMessage(content); + } + } + + return new AssistantMessage(content != null ? content : ""); + } + + private static Message decodeToolResponseMessage(@Nullable String content) { + if (content != null && content.startsWith(TOOL_RESPONSES_JSON_PREFIX)) { + try { + List responses = + OBJECT_MAPPER.readValue( + content.substring(TOOL_RESPONSES_JSON_PREFIX.length()), TOOL_RESPONSE_LIST_TYPE); + + return ToolResponseMessage.builder() + .responses(responses) + .build(); + } catch (JsonProcessingException e) { + return ToolResponseMessage.builder() + .responses(List.of()) + .build(); + } + } + + return ToolResponseMessage.builder() + .responses(List.of()) + .build(); + } } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java index 421d1808957..f2c4dc2844c 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java @@ -33,6 +33,11 @@ import com.bytechef.platform.configuration.domain.ClusterElement; import com.bytechef.platform.configuration.domain.ClusterElementMap; import java.util.Map; +import java.util.Objects; +import org.springframework.ai.chat.client.ChatClientRequest; +import org.springframework.ai.chat.client.ChatClientResponse; +import org.springframework.ai.chat.client.advisor.api.AdvisorChain; +import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor.Builder; @@ -99,6 +104,45 @@ protected ChatMemoryFunction.Result apply( .defaultTopK( inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)); - return new ChatMemoryFunction.Result(builder.build(), null); + return new ChatMemoryFunction.Result(new ToolCallSafeVectorStoreChatMemoryAdvisor(builder.build()), null); + } + + /** + * Guards {@link VectorStoreChatMemoryAdvisor} against the second iteration of a {@code ToolCallAdvisor} loop. In + * that iteration the prompt has only a {@code SystemMessage} and a {@code ToolResponseMessage} — no real user + * message — so {@code getUserMessage().getText()} is blank. Passing a blank string to the embedding API causes a + * 400 error. When there is no real user message we skip the vector-store lookup and pass the request through + * unchanged; the {@code after()} delegation is preserved so the final assistant reply is still stored. + */ + private static class ToolCallSafeVectorStoreChatMemoryAdvisor implements BaseChatMemoryAdvisor { + + private final VectorStoreChatMemoryAdvisor delegate; + + ToolCallSafeVectorStoreChatMemoryAdvisor(VectorStoreChatMemoryAdvisor delegate) { + this.delegate = delegate; + } + + @Override + public ChatClientRequest before(ChatClientRequest request, AdvisorChain advisorChain) { + String userText = Objects.requireNonNullElse(request.prompt() + .getUserMessage() + .getText(), ""); + + if (userText.isBlank()) { + return request; + } + + return delegate.before(request, advisorChain); + } + + @Override + public ChatClientResponse after(ChatClientResponse response, AdvisorChain advisorChain) { + return delegate.after(response, advisorChain); + } + + @Override + public int getOrder() { + return delegate.getOrder(); + } } } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index ab8f9093e97..ad35ca80e84 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -68,6 +68,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.memory.ChatMemory; @@ -359,12 +360,17 @@ List getAdvisors( if (chatMemoryAdvisor != null) { advisors.add(chatMemoryAdvisor); - // Spring AI auto-applies this when ToolCallAdvisor is auto-registered (see DefaultChatClient - // .autoRegisterToolCallAdvisor), but we register manually to supply our own ToolCallingManager, - // so the auto-disable path is skipped and we must call it ourselves. Removing this line makes - // ToolCallAdvisor carry full conversation history alongside the downstream ChatMemoryAdvisor — - // double bookkeeping that produces malformed tool-call sequences on OpenAI. - toolCallAdvisorBuilder.disableInternalConversationHistory(); + // Disable ToolCallAdvisor's internal conversation history only when a MessageChatMemoryAdvisor + // is present. That advisor adds previous messages to the prompt directly, so ToolCallAdvisor + // must not duplicate them (double bookkeeping → malformed tool-call sequences on OpenAI). + // + // VectorStoreChatMemoryAdvisor is NOT a MessageChatMemoryAdvisor — it only augments the system + // message via semantic search and does not inject conversation messages into the prompt. With it, + // ToolCallAdvisor must keep its internal history so the second loop iteration includes the + // AssistantMessage(toolCalls) that OpenAI requires before the ToolResponseMessage. + if (chatMemoryAdvisor instanceof MessageChatMemoryAdvisor) { + toolCallAdvisorBuilder.disableInternalConversationHistory(); + } } advisors.add(toolCallAdvisorBuilder.build()); diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index 927f67459f2..6a36d7f1e7a 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -501,17 +501,37 @@ void testGetAdvisorsIncludesToolCallAdvisorWithDefaultConversationHistoryWhenNoC } @Test - void testGetAdvisorsAddsChatMemoryBeforeToolCallAdvisorAndDisablesInternalConversationHistory() throws Exception { - Map chatMemoryElement = new HashMap<>(); + void testGetAdvisorsDisablesConversationHistoryForMessageChatMemoryAdvisor() throws Exception { + ClusterElementMap clusterElementMap = buildClusterElementMapWithMemory(); - chatMemoryElement.put("name", "memory_1"); - chatMemoryElement.put("type", "memoryComponent/v1/memoryElement"); - chatMemoryElement.put("parameters", Map.of()); + MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor + .builder(mock(ChatMemory.class)) + .build(); - ClusterElementMap clusterElementMap = ClusterElementMap.of( - Map.of( - "clusterElements", - Map.of("model", buildModelClusterElement(), "chatMemory", chatMemoryElement))); + ChatMemoryFunction chatMemoryFunction = mock(ChatMemoryFunction.class); + + when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(chatMemoryAdvisor); + when(clusterElementDefinitionService.getClusterElement( + eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); + + Map connectionParameters = Map.of( + "memory_1", new ComponentConnection("memoryComponent", 1, 2L, Map.of(), null)); + + List advisors = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) + .getAdvisors(clusterElementMap, connectionParameters, mock(ActionContext.class)); + + int chatMemoryIndex = advisors.indexOf(chatMemoryAdvisor); + ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); + + assertThat(chatMemoryIndex).isGreaterThanOrEqualTo(0); + assertThat(advisors.indexOf(toolCallAdvisor)).isGreaterThan(chatMemoryIndex); + assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isFalse(); + } + + @Test + void testGetAdvisorsKeepsConversationHistoryForNonMessageChatMemoryAdvisor() throws Exception { + ClusterElementMap clusterElementMap = buildClusterElementMapWithMemory(); BaseChatMemoryAdvisor chatMemoryAdvisor = mock(BaseChatMemoryAdvisor.class); @@ -521,24 +541,31 @@ void testGetAdvisorsAddsChatMemoryBeforeToolCallAdvisorAndDisablesInternalConver when(clusterElementDefinitionService.getClusterElement( eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); - ComponentConnection memoryConnection = new ComponentConnection( - "memoryComponent", 1, 2L, Map.of(), null); - - Map connectionParameters = Map.of("memory_1", memoryConnection); - ActionContext actionContext = mock(ActionContext.class); - - TestAiAgentChatAction action = new TestAiAgentChatAction( - aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); + Map connectionParameters = Map.of( + "memory_1", new ComponentConnection("memoryComponent", 1, 2L, Map.of(), null)); - List advisors = action.getAdvisors(clusterElementMap, connectionParameters, actionContext); + List advisors = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) + .getAdvisors(clusterElementMap, connectionParameters, mock(ActionContext.class)); int chatMemoryIndex = advisors.indexOf(chatMemoryAdvisor); ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); - int toolCallIndex = advisors.indexOf(toolCallAdvisor); assertThat(chatMemoryIndex).isGreaterThanOrEqualTo(0); - assertThat(toolCallIndex).isGreaterThan(chatMemoryIndex); - assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isFalse(); + assertThat(advisors.indexOf(toolCallAdvisor)).isGreaterThan(chatMemoryIndex); + assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isTrue(); + } + + private static ClusterElementMap buildClusterElementMapWithMemory() { + Map chatMemoryElement = new HashMap<>(); + + chatMemoryElement.put("name", "memory_1"); + chatMemoryElement.put("type", "memoryComponent/v1/memoryElement"); + chatMemoryElement.put("parameters", Map.of()); + + return ClusterElementMap.of( + Map.of("clusterElements", + Map.of("model", buildModelClusterElement(), "chatMemory", chatMemoryElement))); } private static Map buildModelClusterElement() { From 5e2c604d592d5255136dfd6361ec05d124c88db3 Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Wed, 3 Jun 2026 18:21:10 +0200 Subject: [PATCH 15/24] 1652 - changed deprecated JsonParser --- .../component/ai/agent/action/AbstractAiAgentChatAction.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index ad35ca80e84..7d711945755 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -80,8 +80,8 @@ import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.augment.AugmentedToolCallbackProvider; import org.springframework.ai.tool.definition.ToolDefinition; -import org.springframework.ai.util.json.JsonParser; import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; /** * @author Ivica Cardic @@ -89,6 +89,7 @@ public abstract class AbstractAiAgentChatAction { private static final Logger log = LoggerFactory.getLogger(AbstractAiAgentChatAction.class); + private static final JsonMapper JSON_MAPPER = new JsonMapper(); private static final String TOOL_SIMULATION_UNAVAILABLE = "[tool simulation unavailable]"; @@ -222,7 +223,7 @@ private String observeAndCall(String toolInput, Supplier execution) { Map inputs; try { - inputs = JsonParser.fromJson(toolInput, new TypeReference<>() {}); + inputs = JSON_MAPPER.readValue(toolInput, new TypeReference<>() {}); } catch (Exception exception) { context.log( log -> log.debug( From 3e512e6a922e7278e541237030217cc3fa861cf9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:12:00 +0000 Subject: [PATCH 16/24] test: update mistral component snapshot for current definition --- .../ai/llm/mistral/src/test/resources/definition/mistral_v1.json | 1 - 1 file changed, 1 deletion(-) diff --git a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json index cfeac17fda3..b71bed8f8a5 100644 --- a/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json +++ b/server/libs/modules/components/ai/llm/mistral/src/test/resources/definition/mistral_v1.json @@ -2697,7 +2697,6 @@ "customActionHelp": null, "description": "Open, efficient, helpful and trustworthy AI models through ground-breaking innovations.", "icon": "path:assets/mistral.svg", - "inputs": null, "metadata": null, "name": "mistral", "resources": null, From 6d24bb4c4db3c858fc81beb6ceaf5f4f916aaf04 Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Tue, 2 Jun 2026 15:34:57 +0200 Subject: [PATCH 17/24] 1570 Move workflow validate and instruction MCP tools to mcp-tool-platform --- .../copilot/config/CopilotConfiguration.java | 51 ++++++++-- .../ManagementMcpServerConfiguration.java | 5 +- .../tool/automation/ProjectWorkflowTools.java | 84 +---------------- .../automation/ReadProjectWorkflowTools.java | 23 ----- .../model/WorkflowValidationResult.java | 33 ------- .../automation/ProjectWorkflowToolsTest.java | 19 +--- .../platform/WorkflowInstructionTools.java | 90 ++++++++++++++++++ .../tool/platform/WorkflowValidatorTools.java | 94 +++++++++++++++++++ .../WorkflowValidatorToolErrorType.java | 31 ++++++ .../instruction_cluster_elements.txt | 0 .../resources/instruction_script_code.txt | 0 .../resources/instruction_workflow_build.txt | 0 12 files changed, 263 insertions(+), 167 deletions(-) delete mode 100644 server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/model/WorkflowValidationResult.java create mode 100644 server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowInstructionTools.java create mode 100644 server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowValidatorTools.java create mode 100644 server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java rename server/libs/ai/mcp/mcp-tool/{mcp-tool-automation => mcp-tool-platform}/src/main/resources/instruction_cluster_elements.txt (100%) rename server/libs/ai/mcp/mcp-tool/{mcp-tool-automation => mcp-tool-platform}/src/main/resources/instruction_script_code.txt (100%) rename server/libs/ai/mcp/mcp-tool/{mcp-tool-automation => mcp-tool-platform}/src/main/resources/instruction_workflow_build.txt (100%) diff --git a/server/ee/libs/ai/ai-copilot/ai-copilot-service/src/main/java/com/bytechef/ee/ai/copilot/config/CopilotConfiguration.java b/server/ee/libs/ai/ai-copilot/ai-copilot-service/src/main/java/com/bytechef/ee/ai/copilot/config/CopilotConfiguration.java index a650de4ad94..64de7802001 100644 --- a/server/ee/libs/ai/ai-copilot/ai-copilot-service/src/main/java/com/bytechef/ee/ai/copilot/config/CopilotConfiguration.java +++ b/server/ee/libs/ai/ai-copilot/ai-copilot-service/src/main/java/com/bytechef/ee/ai/copilot/config/CopilotConfiguration.java @@ -20,6 +20,8 @@ import com.bytechef.ai.mcp.tool.platform.ComponentTools; import com.bytechef.ai.mcp.tool.platform.FirecrawlTools; import com.bytechef.ai.mcp.tool.platform.TaskTools; +import com.bytechef.ai.mcp.tool.platform.WorkflowInstructionTools; +import com.bytechef.ai.mcp.tool.platform.WorkflowValidatorTools; import com.bytechef.atlas.configuration.service.WorkflowService; import com.bytechef.ee.ai.copilot.agent.ClusterElementSpringAIAgent; import com.bytechef.ee.ai.copilot.agent.CodeEditorSpringAIAgent; @@ -64,6 +66,8 @@ public class CopilotConfiguration { private final Resource promptClusterElementBuildResource; private final Resource promptSkillsAskResource; private final Resource promptSkillsBuildResource; + private final WorkflowValidatorTools workflowValidatorTools; + private final WorkflowInstructionTools workflowInstructionTools; private final State state = new State(); @SuppressFBWarnings("EI") @@ -76,8 +80,11 @@ public CopilotConfiguration( @Value("classpath:prompt_cluster_element_ask.txt") Resource promptClusterElementAskResource, @Value("classpath:prompt_cluster_element_build.txt") Resource promptClusterElementBuildResource, @Value("classpath:prompt_skills_ask.txt") Resource promptSkillsAskResource, - @Value("classpath:prompt_skills_build.txt") Resource promptSkillsBuildResource) { + @Value("classpath:prompt_skills_build.txt") Resource promptSkillsBuildResource, + WorkflowValidatorTools workflowValidatorTools, WorkflowInstructionTools workflowInstructionTools) { + this.workflowValidatorTools = workflowValidatorTools; + this.workflowInstructionTools = workflowInstructionTools; this.promptWorkflowEditorAskResource = promptWorkflowEditorAskResource; this.promptWorkflowEditorBuildResource = promptWorkflowEditorBuildResource; this.promptCodeEditorAskResource = promptCodeEditorAskResource; @@ -95,7 +102,8 @@ CodeEditorSpringAIAgent codeEditorAskSpringAIAgent( ComponentTools componentTools, Optional firecrawlTools) throws AGUIException { String name = Source.CODE_EDITOR.name() + "_" + Mode.ASK.name(); - List tools = new ArrayList<>(List.of(readProjectWorkflowTools, componentTools)); + List tools = new ArrayList<>( + List.of(readProjectWorkflowTools, componentTools, workflowValidatorTools, workflowInstructionTools)); firecrawlTools.ifPresent(tools::add); @@ -123,7 +131,10 @@ CodeEditorSpringAIAgent codeEditorBuildSpringAIAgent( .chatMemory(chatMemory) .chatModel(chatModel) .systemMessage(getSystemPrompt(promptCodeEditorBuildResource)) - .tools(List.of(readProjectWorkflowTools, scriptTools, componentTools)) + .tools( + List.of( + readProjectWorkflowTools, scriptTools, componentTools, workflowValidatorTools, + workflowInstructionTools)) .state(state) .build(); } @@ -140,7 +151,10 @@ ClusterElementSpringAIAgent clusterElementAskSpringAIAgent( .chatMemory(chatMemory) .chatModel(chatModel) .systemMessage(getSystemPrompt(promptClusterElementAskResource)) - .tools(List.of(readProjectWorkflowTools, componentTools, taskTools)) + .tools( + List.of( + readProjectWorkflowTools, componentTools, taskTools, workflowValidatorTools, + workflowInstructionTools)) .state(state) .build(); } @@ -158,7 +172,10 @@ ClusterElementSpringAIAgent clusterElementBuildSpringAIAgent( .chatMemory(chatMemory) .chatModel(chatModel) .systemMessage(getSystemPrompt(promptClusterElementBuildResource)) - .tools(List.of(readProjectWorkflowTools, clusterElementTools, componentTools, taskTools)) + .tools( + List.of( + readProjectWorkflowTools, clusterElementTools, componentTools, taskTools, workflowValidatorTools, + workflowInstructionTools)) .state(state) .build(); } @@ -180,7 +197,9 @@ WorkflowEditorSpringAIAgent workflowEditorAskSpringAIAgent( String name = Source.WORKFLOW_EDITOR.name() + "_" + Mode.ASK.name(); List tools = new ArrayList<>( - List.of(readProjectTools, readProjectWorkflowTools, componentTools, taskTools)); + List.of( + readProjectTools, readProjectWorkflowTools, componentTools, taskTools, workflowValidatorTools, + workflowInstructionTools)); firecrawlTools.ifPresent(tools::add); @@ -212,7 +231,10 @@ WorkflowEditorSpringAIAgent workflowEditorBuildSpringAIAgent( .chatModel(chatModel) .systemMessage(getSystemPrompt(promptWorkflowEditorBuildResource)) .state(state) - .tools(List.of(projectTools, projectWorkflowTools, taskTools, scriptTools)) + .tools( + List.of( + projectTools, projectWorkflowTools, taskTools, scriptTools, workflowValidatorTools, + workflowInstructionTools)) .workflowService(workflowService) .workflowNodeOutputFacade(workflowNodeOutputFacade) .build(); @@ -232,7 +254,10 @@ ConverterSpringAIAgent converterBuildSpringAIAgent( .chatModel(chatModel) .systemMessage(getSystemPrompt(promptConverterBuildResource)) .state(state) - .tools(List.of(projectToolsImpl, projectWorkflowToolsImpl, taskTools, scriptTools)) + .tools( + List.of( + projectToolsImpl, projectWorkflowToolsImpl, taskTools, scriptTools, workflowValidatorTools, + workflowInstructionTools)) .build(); } @@ -250,7 +275,10 @@ SkillsSpringAIAgent skillsAskSpringAIAgent( .chatModel(chatModel) .systemMessage(getSystemPrompt(promptSkillsAskResource)) .state(state) - .tools(List.of(readSkillsTools, readProjectTools, readProjectWorkflowTools)) + .tools( + List.of( + readSkillsTools, readProjectTools, readProjectWorkflowTools, workflowValidatorTools, + workflowInstructionTools)) .build(); } @@ -268,7 +296,10 @@ SkillsSpringAIAgent skillsBuildSpringAIAgent( .chatModel(chatModel) .systemMessage(getSystemPrompt(promptSkillsBuildResource)) .state(state) - .tools(List.of(skillsTools, readProjectTools, readProjectWorkflowTools)) + .tools( + List.of( + skillsTools, readProjectTools, readProjectWorkflowTools, workflowValidatorTools, + workflowInstructionTools)) .build(); } diff --git a/server/libs/ai/mcp/mcp-server/src/main/java/com/bytechef/ai/mcp/server/config/ManagementMcpServerConfiguration.java b/server/libs/ai/mcp/mcp-server/src/main/java/com/bytechef/ai/mcp/server/config/ManagementMcpServerConfiguration.java index 5efd23f988b..b22d4be149a 100644 --- a/server/libs/ai/mcp/mcp-server/src/main/java/com/bytechef/ai/mcp/server/config/ManagementMcpServerConfiguration.java +++ b/server/libs/ai/mcp/mcp-server/src/main/java/com/bytechef/ai/mcp/server/config/ManagementMcpServerConfiguration.java @@ -77,8 +77,9 @@ public class ManagementMcpServerConfiguration { @SuppressFBWarnings("EI") public ManagementMcpServerConfiguration( ComponentTools componentTools, @Nullable FirecrawlTools firecrawlTools, ProjectTools projectTools, - ProjectWorkflowTools projectWorkflowTools, TaskTools taskTools, TaskDispatcherTools taskDispatcherTools, - ScriptTools scriptTools, SkillsTools skillsTools, ClusterElementTools clusterElementTools) { + ProjectWorkflowTools projectWorkflowTools, TaskTools taskTools, ScriptTools scriptTools, + TaskDispatcherTools taskDispatcherTools, SkillsTools skillsTools, + ClusterElementTools clusterElementTools) { this.componentTools = componentTools; this.firecrawlTools = firecrawlTools; diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowTools.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowTools.java index 1aca22a8a95..4d790d7624c 100644 --- a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowTools.java +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowTools.java @@ -21,24 +21,17 @@ import com.bytechef.ai.mcp.tool.automation.exception.ProjectWorkflowToolErrorType; import com.bytechef.ai.mcp.tool.automation.model.ProjectWorkflowInfo; import com.bytechef.ai.mcp.tool.automation.model.WorkflowInfo; -import com.bytechef.ai.mcp.tool.automation.model.WorkflowValidationResult; import com.bytechef.automation.configuration.domain.ProjectWorkflow; import com.bytechef.automation.configuration.dto.ProjectWorkflowDTO; import com.bytechef.automation.configuration.facade.ProjectWorkflowFacade; import com.bytechef.exception.ExecutionException; -import com.bytechef.platform.workflow.validator.WorkflowValidatorFacade; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; /** @@ -70,35 +63,10 @@ public class ProjectWorkflowTools { """; private final ProjectWorkflowFacade projectWorkflowFacade; - private final String scriptCodeInstructions; - private final String workflowBuildInstructions; - private final String clusterElementsInstructions; - private final WorkflowValidatorFacade workflowValidatorFacade; - - @SuppressFBWarnings({ - "CT_CONSTRUCTOR_THROW", "EI" - }) - public ProjectWorkflowTools( - ProjectWorkflowFacade projectWorkflowFacade, WorkflowValidatorFacade workflowValidatorFacade, - @Value("classpath:instruction_script_code.txt") Resource scriptCodeInstructionsResource, - @Value("classpath:instruction_cluster_elements.txt") Resource clusterElementsInstructionsResource, - @Value("classpath:instruction_workflow_build.txt") Resource workflowBuildInstructionsResource) { + @SuppressFBWarnings("EI") + public ProjectWorkflowTools(ProjectWorkflowFacade projectWorkflowFacade) { this.projectWorkflowFacade = projectWorkflowFacade; - this.scriptCodeInstructions = readResource(scriptCodeInstructionsResource); - this.workflowValidatorFacade = workflowValidatorFacade; - this.workflowBuildInstructions = readResource(workflowBuildInstructionsResource); - this.clusterElementsInstructions = readResource(clusterElementsInstructionsResource); - } - - @Tool(description = "Instructions for writing custom code in Script component") - public String getScriptCodeInstructions() { - return scriptCodeInstructions; - } - - @Tool(description = "Instructions for working with cluster elements") - public String getClusterElementsInstructions() { - return clusterElementsInstructions; } @Tool( @@ -127,12 +95,6 @@ public WorkflowInfo getWorkflow( } } - @SuppressFBWarnings("VA") - @Tool(description = "Instructions for building workflows") - public String getWorkflowBuildInstructions() { - return workflowBuildInstructions.formatted(DEFAULT_DEFINITION); - } - @Tool( description = "List all workflows in a project. Returns a list of workflows with their basic information including id, name and description") public List listWorkflows( @@ -210,40 +172,6 @@ public List searchWorkflows( } } - @Tool( - description = "Validate a workflow configuration by checking its structure, properties and outputs against the task definitions. Returns validation results with any errors found") - public WorkflowValidationResult validateWorkflow( - @ToolParam(description = "The JSON string of the workflow to validate") String workflow) { - - try { - WorkflowValidatorFacade.WorkflowValidationResult workflowValidationResult = - workflowValidatorFacade.validateWorkflow(workflow); - - List errors = workflowValidationResult.errors(); - - String errorMessages = errors.toString(); - - List warnings = workflowValidationResult.warnings(); - - String warningMessages = warnings.toString(); - - boolean isValid = errorMessages.equals("[]"); - - if (log.isDebugEnabled()) { - log.debug( - "validateWorkflow(): Validated workflow. Valid: {}, Errors: {}, Warnings: {}", isValid, - errorMessages, warningMessages); - } - - return new WorkflowValidationResult(isValid, errorMessages, warningMessages); - } catch (Exception e) { - log.error("validateWorkflow(): Failed to validate workflow", e); - - throw new ExecutionException( - "Failed to validate workflow", e, ProjectWorkflowToolErrorType.VALIDATE_WORKFLOW); - } - } - @Tool( description = "Create a new workflow in a ByteChef project. Returns the created workflow information including id, project id, workflow id, and reference code.") public ProjectWorkflowInfo createProjectWorkflow( @@ -331,12 +259,4 @@ public WorkflowInfo updateWorkflow( "Failed to update workflow: " + e.getMessage(), e, ProjectWorkflowToolErrorType.UPDATE_WORKFLOW); } } - - private static String readResource(Resource resource) { - try (InputStream inputStream = resource.getInputStream()) { - return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException ioException) { - throw new IllegalStateException("Failed to read resource: " + resource.getDescription(), ioException); - } - } } diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ReadProjectWorkflowTools.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ReadProjectWorkflowTools.java index fa7d393dd3c..03d4b6cdeec 100644 --- a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ReadProjectWorkflowTools.java +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/ReadProjectWorkflowTools.java @@ -17,7 +17,6 @@ package com.bytechef.ai.mcp.tool.automation; import com.bytechef.ai.mcp.tool.automation.model.WorkflowInfo; -import com.bytechef.ai.mcp.tool.automation.model.WorkflowValidationResult; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.List; import org.springframework.ai.tool.annotation.Tool; @@ -40,16 +39,6 @@ public ReadProjectWorkflowTools(ProjectWorkflowTools projectWorkflowTools) { this.delegate = projectWorkflowTools; } - @Tool( - description = "Instructions for writing custom code in Script component") - public String getScriptCodeInstructions() { - return delegate.getScriptCodeInstructions(); - } - - public String getClusterElementsInstructions() { - return this.delegate.getClusterElementsInstructions(); - } - @Tool( description = "Get comprehensive information about a specific workflow. Returns detailed project information including id, name, description, version, definition, project workflow id, created date, last modified date.") public WorkflowInfo getWorkflow( @@ -57,11 +46,6 @@ public WorkflowInfo getWorkflow( return delegate.getWorkflow(workflowId); } - @Tool(description = "Instructions for building workflows") - public String getWorkflowBuildInstructions() { - return delegate.getWorkflowBuildInstructions(); - } - @Tool( description = "List all workflows in a project. Returns a list of workflows with their basic information including id, name and description") public List listWorkflows( @@ -76,11 +60,4 @@ public List searchWorkflows( @ToolParam(required = false, description = "The ID of the project") Long projectId) { return delegate.searchWorkflows(query, projectId); } - - @Tool( - description = "Validate a workflow configuration by checking its structure, properties and outputs against the task definitions. Returns validation results with any errors found") - public WorkflowValidationResult validateWorkflow( - @ToolParam(description = "The JSON string of the workflow to validate") String workflowId) { - return delegate.validateWorkflow(workflowId); - } } diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/model/WorkflowValidationResult.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/model/WorkflowValidationResult.java deleted file mode 100644 index 8ba79740aca..00000000000 --- a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/java/com/bytechef/ai/mcp/tool/automation/model/WorkflowValidationResult.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.ai.mcp.tool.automation.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Workflow validation result record for API responses. - * - * @author Marko Kriskovic - */ -@SuppressFBWarnings("EI") -public record WorkflowValidationResult( - @JsonProperty("valid") @JsonPropertyDescription("Whether the workflow is valid") boolean valid, - @JsonProperty("errors") @JsonPropertyDescription("Error details, which need to be fixed before the workflow can be valid") String errors, - @JsonProperty("warnings") @JsonPropertyDescription("Warning details that give additional information") String warnings) { -} diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/test/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowToolsTest.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/test/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowToolsTest.java index 96bb4c8edc7..ff7b40fd5ed 100644 --- a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/test/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowToolsTest.java +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/test/java/com/bytechef/ai/mcp/tool/automation/ProjectWorkflowToolsTest.java @@ -16,17 +16,9 @@ package com.bytechef.ai.mcp.tool.automation; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.bytechef.platform.workflow.validator.WorkflowValidatorFacade; -import java.io.ByteArrayInputStream; -import java.io.IOException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.core.io.Resource; /** * @author Marko Kriskovic @@ -34,15 +26,8 @@ @ExtendWith(MockitoExtension.class) class ProjectWorkflowToolsTest { - @Mock - private WorkflowValidatorFacade workflowValidatorFacade; - @Test - void instantiatesSuccessfully() throws IOException { - Resource emptyResource = mock(Resource.class); - - when(emptyResource.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[0])); - - new ProjectWorkflowTools(null, workflowValidatorFacade, emptyResource, emptyResource, emptyResource); + void instantiatesSuccessfully() { + new ProjectWorkflowTools(null); } } diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowInstructionTools.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowInstructionTools.java new file mode 100644 index 00000000000..07519df8c6a --- /dev/null +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowInstructionTools.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.ai.mcp.tool.platform; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +/** + * Platform-level workflow authoring instruction tools. + * + * @author Marko Kriskovic + */ +@Component +public class WorkflowInstructionTools { + + private static final String DEFAULT_DEFINITION = """ + { + "label": "workflowName", + "description": "workflowDescription", + "inputs": [], + "triggers": [ + { + "label": "Manual", + "name": "trigger_1", + "type": "manual/v1/manual" + } + ], + "tasks": [] + } + """; + + private final String clusterElementsInstructions; + private final String scriptCodeInstructions; + private final String workflowBuildInstructions; + + @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") + public WorkflowInstructionTools( + @Value("classpath:instruction_script_code.txt") Resource scriptCodeInstructionsResource, + @Value("classpath:instruction_cluster_elements.txt") Resource clusterElementsInstructionsResource, + @Value("classpath:instruction_workflow_build.txt") Resource workflowBuildInstructionsResource) { + + this.scriptCodeInstructions = readResource(scriptCodeInstructionsResource); + this.clusterElementsInstructions = readResource(clusterElementsInstructionsResource); + this.workflowBuildInstructions = readResource(workflowBuildInstructionsResource); + } + + @Tool(description = "Instructions for working with cluster elements") + public String getClusterElementsInstructions() { + return clusterElementsInstructions; + } + + @Tool(description = "Instructions for writing custom code in Script component") + public String getScriptCodeInstructions() { + return scriptCodeInstructions; + } + + @SuppressFBWarnings("VA") + @Tool(description = "Instructions for building workflows") + public String getWorkflowBuildInstructions() { + return workflowBuildInstructions.formatted(DEFAULT_DEFINITION); + } + + private static String readResource(Resource resource) { + try (InputStream inputStream = resource.getInputStream()) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException ioException) { + throw new IllegalStateException("Failed to read resource: " + resource.getDescription(), ioException); + } + } +} diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowValidatorTools.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowValidatorTools.java new file mode 100644 index 00000000000..8eb9838c055 --- /dev/null +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/WorkflowValidatorTools.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.ai.mcp.tool.platform; + +import com.bytechef.ai.mcp.tool.platform.exception.WorkflowValidatorToolErrorType; +import com.bytechef.exception.ExecutionException; +import com.bytechef.platform.workflow.validator.WorkflowValidatorFacade; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +/** + * Platform-level workflow validation tool. + * + * @author Marko Kriskovic + */ +@Component +public class WorkflowValidatorTools { + + private static final Logger log = LoggerFactory.getLogger(WorkflowValidatorTools.class); + + private final WorkflowValidatorFacade workflowValidatorFacade; + + @SuppressFBWarnings("EI") + public WorkflowValidatorTools(WorkflowValidatorFacade workflowValidatorFacade) { + this.workflowValidatorFacade = workflowValidatorFacade; + } + + @Tool( + description = "Validate a workflow configuration by checking its structure, properties and outputs against the task definitions. Returns validation results with any errors found") + public WorkflowValidationResult validateWorkflow( + @ToolParam(description = "The JSON string of the workflow to validate") String workflow) { + + try { + WorkflowValidatorFacade.WorkflowValidationResult workflowValidationResult = + workflowValidatorFacade.validateWorkflow(workflow); + + List errors = workflowValidationResult.errors(); + + String errorMessages = errors.toString(); + + List warnings = workflowValidationResult.warnings(); + + String warningMessages = warnings.toString(); + + boolean isValid = errorMessages.equals("[]"); + + if (log.isDebugEnabled()) { + log.debug( + "validateWorkflow(): Validated workflow. Valid: {}, Errors: {}, Warnings: {}", isValid, + errorMessages, warningMessages); + } + + return new WorkflowValidationResult(isValid, errorMessages, warningMessages); + } catch (Exception e) { + log.error("validateWorkflow(): Failed to validate workflow", e); + + throw new ExecutionException( + "Failed to validate workflow", e, WorkflowValidatorToolErrorType.VALIDATE_WORKFLOW); + } + } + + /** + * Workflow validation result record for API responses. + * + * @author Marko Kriskovic + */ + @SuppressFBWarnings("EI") + public record WorkflowValidationResult( + @JsonProperty("valid") @JsonPropertyDescription("Whether the workflow is valid") boolean valid, + @JsonProperty("errors") @JsonPropertyDescription("Error details, which need to be fixed before the workflow can be valid") String errors, + @JsonProperty("warnings") @JsonPropertyDescription("Warning details that give additional information") String warnings) { + } +} diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java new file mode 100644 index 00000000000..9bcfaf18a45 --- /dev/null +++ b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.ai.mcp.tool.platform.exception; + +import com.bytechef.exception.AbstractErrorType; + +/** + * @author Marko Kriskovic + */ +public class WorkflowValidatorToolErrorType extends AbstractErrorType { + + public static final WorkflowValidatorToolErrorType VALIDATE_WORKFLOW = new WorkflowValidatorToolErrorType(100); + + private WorkflowValidatorToolErrorType(int errorKey) { + super(WorkflowValidatorToolErrorType.class, errorKey); + } +} diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_cluster_elements.txt b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_cluster_elements.txt similarity index 100% rename from server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_cluster_elements.txt rename to server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_cluster_elements.txt diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_script_code.txt b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_script_code.txt similarity index 100% rename from server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_script_code.txt rename to server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_script_code.txt diff --git a/server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_workflow_build.txt b/server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_workflow_build.txt similarity index 100% rename from server/libs/ai/mcp/mcp-tool/mcp-tool-automation/src/main/resources/instruction_workflow_build.txt rename to server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/resources/instruction_workflow_build.txt From 7f86f63cab008a95fdefa50edf7c04034e03d1e2 Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Fri, 5 Jun 2026 09:56:15 +0200 Subject: [PATCH 18/24] 1652 - using filter in Chat memory directly --- .../memory/builtin/cluster/ChatMemory.java | 3 +- .../memory/jdbc/cluster/JdbcChatMemory.java | 3 +- .../jdbc/util/FilteringWindowChatMemory.java | 60 +++++++++++++++++++ .../util/OrderedJdbcChatMemoryRepository.java | 14 +++-- 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java index 73ca369496f..f4afd75abdf 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java @@ -21,6 +21,7 @@ import static com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction.CHAT_MEMORY; import com.bytechef.component.ai.agent.chat.memory.builtin.util.ChatMemoryUtils; +import com.bytechef.component.ai.agent.chat.memory.jdbc.util.FilteringWindowChatMemory; import com.bytechef.component.definition.ClusterElementDefinition; import com.bytechef.component.definition.ComponentDsl; import com.bytechef.component.definition.Parameters; @@ -61,7 +62,7 @@ protected static ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(chatMemory) + MessageChatMemoryAdvisor.builder(new FilteringWindowChatMemory(chatMemory)) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java index 6a99fb953dd..65b8344e393 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java @@ -20,6 +20,7 @@ import static com.bytechef.component.definition.ComponentDsl.string; import static com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction.CHAT_MEMORY; +import com.bytechef.component.ai.agent.chat.memory.jdbc.util.FilteringWindowChatMemory; import com.bytechef.component.ai.agent.chat.memory.jdbc.util.JdbcChatMemoryUtils; import com.bytechef.component.definition.ClusterElementDefinition; import com.bytechef.component.definition.ComponentDsl; @@ -73,7 +74,7 @@ protected ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(chatMemory) + MessageChatMemoryAdvisor.builder(new FilteringWindowChatMemory(chatMemory)) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java new file mode 100644 index 00000000000..46077a7a751 --- /dev/null +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.agent.chat.memory.jdbc.util; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.List; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.ai.chat.messages.Message; + +/** + * Wraps {@link MessageWindowChatMemory} and re-applies tool-call sequence filtering after the message window is + * applied. The underlying {@link org.springframework.ai.chat.memory.ChatMemoryRepository} may already filter broken + * sequences from stored data, but the window slice can still produce orphaned + * {@link org.springframework.ai.chat.messages.ToolResponseMessage} objects when it cuts a valid + * AssistantMessage+ToolResponseMessage pair. This wrapper ensures the filtered view is always consistent before it + * reaches the LLM. + * + * @author ByteChef + */ +public class FilteringWindowChatMemory implements ChatMemory { + + private final MessageWindowChatMemory delegate; + + @SuppressFBWarnings("EI2") + public FilteringWindowChatMemory(MessageWindowChatMemory delegate) { + this.delegate = delegate; + } + + @Override + public void add(String conversationId, List messages) { + delegate.add(conversationId, messages); + } + + @Override + public List get(String conversationId) { + List messages = delegate.get(conversationId); + + return OrderedJdbcChatMemoryRepository.filterBrokenToolCallSequences(messages); + } + + @Override + public void clear(String conversationId) { + delegate.clear(conversationId); + } +} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java index 6b1420a40e3..e0dbfd373c7 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java @@ -148,12 +148,14 @@ public void deleteByConversationId(String conversationId) { *
    *
  • An {@link AssistantMessage} with tool calls followed by one or more empty {@link ToolResponseMessage} objects * (broken cross-turn sequences from old storage).
  • - *
  • Orphaned empty {@link ToolResponseMessage} objects not preceded by an AssistantMessage with tool calls.
  • + *
  • Orphaned {@link ToolResponseMessage} objects (empty or non-empty) not immediately preceded by an + * {@link AssistantMessage} with tool calls. This covers both stale data and sequences truncated by the message + * window.
  • *
* Within-turn safety: the {@link AssistantMessage} with tool calls is saved before its {@link ToolResponseMessage}, * so mid-turn reads contain only the former and it is preserved intact. */ - static List filterBrokenToolCallSequences(List messages) { + public static List filterBrokenToolCallSequences(List messages) { List result = new ArrayList<>(messages.size()); for (int i = 0; i < messages.size(); i++) { @@ -173,8 +175,12 @@ static List filterBrokenToolCallSequences(List messages) { continue; } - } else if (message instanceof ToolResponseMessage toolResponse && toolResponse.getResponses() - .isEmpty()) { + } else if (message instanceof ToolResponseMessage + && (result.isEmpty() + || !(result.get(result.size() - 1) instanceof AssistantMessage precedingMsg + && precedingMsg.hasToolCalls()))) { + // Covers stale empty-response records from old storage AND window-truncated orphans where + // the AssistantMessage was cut off by the message window. continue; } From 245e5ac9cf59ceb070dd83a29a7a2ec7bb8b2974 Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Fri, 5 Jun 2026 12:20:46 +0200 Subject: [PATCH 19/24] Revert "1652 - using filter in Chat memory directly" This reverts commit 77f054e8a9bb29827e67673c71f80632d4906069. --- .../memory/builtin/cluster/ChatMemory.java | 3 +- .../memory/jdbc/cluster/JdbcChatMemory.java | 3 +- .../jdbc/util/FilteringWindowChatMemory.java | 60 ------------------- .../util/OrderedJdbcChatMemoryRepository.java | 14 ++--- 4 files changed, 6 insertions(+), 74 deletions(-) delete mode 100644 server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java index f4afd75abdf..73ca369496f 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java @@ -21,7 +21,6 @@ import static com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction.CHAT_MEMORY; import com.bytechef.component.ai.agent.chat.memory.builtin.util.ChatMemoryUtils; -import com.bytechef.component.ai.agent.chat.memory.jdbc.util.FilteringWindowChatMemory; import com.bytechef.component.definition.ClusterElementDefinition; import com.bytechef.component.definition.ComponentDsl; import com.bytechef.component.definition.Parameters; @@ -62,7 +61,7 @@ protected static ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(new FilteringWindowChatMemory(chatMemory)) + MessageChatMemoryAdvisor.builder(chatMemory) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java index 65b8344e393..6a99fb953dd 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/cluster/JdbcChatMemory.java @@ -20,7 +20,6 @@ import static com.bytechef.component.definition.ComponentDsl.string; import static com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction.CHAT_MEMORY; -import com.bytechef.component.ai.agent.chat.memory.jdbc.util.FilteringWindowChatMemory; import com.bytechef.component.ai.agent.chat.memory.jdbc.util.JdbcChatMemoryUtils; import com.bytechef.component.definition.ClusterElementDefinition; import com.bytechef.component.definition.ComponentDsl; @@ -74,7 +73,7 @@ protected ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(new FilteringWindowChatMemory(chatMemory)) + MessageChatMemoryAdvisor.builder(chatMemory) .build(), chatMemory); } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java deleted file mode 100644 index 46077a7a751..00000000000 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/FilteringWindowChatMemory.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 ByteChef - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bytechef.component.ai.agent.chat.memory.jdbc.util; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.List; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.memory.MessageWindowChatMemory; -import org.springframework.ai.chat.messages.Message; - -/** - * Wraps {@link MessageWindowChatMemory} and re-applies tool-call sequence filtering after the message window is - * applied. The underlying {@link org.springframework.ai.chat.memory.ChatMemoryRepository} may already filter broken - * sequences from stored data, but the window slice can still produce orphaned - * {@link org.springframework.ai.chat.messages.ToolResponseMessage} objects when it cuts a valid - * AssistantMessage+ToolResponseMessage pair. This wrapper ensures the filtered view is always consistent before it - * reaches the LLM. - * - * @author ByteChef - */ -public class FilteringWindowChatMemory implements ChatMemory { - - private final MessageWindowChatMemory delegate; - - @SuppressFBWarnings("EI2") - public FilteringWindowChatMemory(MessageWindowChatMemory delegate) { - this.delegate = delegate; - } - - @Override - public void add(String conversationId, List messages) { - delegate.add(conversationId, messages); - } - - @Override - public List get(String conversationId) { - List messages = delegate.get(conversationId); - - return OrderedJdbcChatMemoryRepository.filterBrokenToolCallSequences(messages); - } - - @Override - public void clear(String conversationId) { - delegate.clear(conversationId); - } -} diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java index e0dbfd373c7..6b1420a40e3 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java @@ -148,14 +148,12 @@ public void deleteByConversationId(String conversationId) { *
    *
  • An {@link AssistantMessage} with tool calls followed by one or more empty {@link ToolResponseMessage} objects * (broken cross-turn sequences from old storage).
  • - *
  • Orphaned {@link ToolResponseMessage} objects (empty or non-empty) not immediately preceded by an - * {@link AssistantMessage} with tool calls. This covers both stale data and sequences truncated by the message - * window.
  • + *
  • Orphaned empty {@link ToolResponseMessage} objects not preceded by an AssistantMessage with tool calls.
  • *
* Within-turn safety: the {@link AssistantMessage} with tool calls is saved before its {@link ToolResponseMessage}, * so mid-turn reads contain only the former and it is preserved intact. */ - public static List filterBrokenToolCallSequences(List messages) { + static List filterBrokenToolCallSequences(List messages) { List result = new ArrayList<>(messages.size()); for (int i = 0; i < messages.size(); i++) { @@ -175,12 +173,8 @@ public static List filterBrokenToolCallSequences(List messages continue; } - } else if (message instanceof ToolResponseMessage - && (result.isEmpty() - || !(result.get(result.size() - 1) instanceof AssistantMessage precedingMsg - && precedingMsg.hasToolCalls()))) { - // Covers stale empty-response records from old storage AND window-truncated orphans where - // the AssistantMessage was cut off by the message window. + } else if (message instanceof ToolResponseMessage toolResponse && toolResponse.getResponses() + .isEmpty()) { continue; } From 1ba97cb68aafec731603c74168f0dbbf665273ab Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Fri, 5 Jun 2026 12:20:53 +0200 Subject: [PATCH 20/24] Revert "1652 - workaround for deleting toolCallAdvisorBuilder.disableInternalConversationHistory()" This reverts commit 0be576fbd3e7e385528ae8151d7a78350ea1e49f. --- .../chat-memory-jdbc/build.gradle.kts | 1 - .../util/OrderedJdbcChatMemoryRepository.java | 208 +----------------- .../memory/cluster/VectorStoreChatMemory.java | 46 +--- .../action/AbstractAiAgentChatAction.java | 18 +- .../action/AbstractAiAgentChatActionTest.java | 69 ++---- 5 files changed, 32 insertions(+), 310 deletions(-) diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts index a6034ce2506..e115c614be8 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/build.gradle.kts @@ -1,4 +1,3 @@ dependencies { - implementation("com.fasterxml.jackson.core:jackson-databind") implementation("org.springframework.ai:spring-ai-model-chat-memory-repository-jdbc") } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java index 6b1420a40e3..8d563a2239a 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-jdbc/src/main/java/com/bytechef/component/ai/agent/chat/memory/jdbc/util/OrderedJdbcChatMemoryRepository.java @@ -16,59 +16,23 @@ package com.bytechef.component.ai.agent.chat.memory.jdbc.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import javax.sql.DataSource; -import org.jspecify.annotations.Nullable; import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepositoryDialect; -import org.springframework.ai.chat.memory.repository.jdbc.PostgresChatMemoryRepositoryDialect; -import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.MessageType; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.ToolResponseMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; /** - * Wraps a {@link ChatMemoryRepository} and: - *
    - *
  • Replaces {@link #findConversationIds()} with a query ordered by most-recent timestamp DESC.
  • - *
  • Serializes {@link AssistantMessage#getToolCalls()} and {@link ToolResponseMessage#getResponses()} as JSON in the - * {@code content} column so that tool-call sequences survive a DB round-trip. Spring AI's built-in - * {@code JdbcChatMemoryRepository} discards this information (always stores {@code ""} for both message types).
  • - *
  • Filters out broken tool-call sequences on read, preventing 400 errors from the LLM API.
  • - *
+ * Wraps a {@link ChatMemoryRepository} and replaces {@link #findConversationIds()} with a query that orders results by + * the most recent message timestamp (DESC) so callers always see the most active conversations first. * * @author ByteChef */ public class OrderedJdbcChatMemoryRepository implements ChatMemoryRepository { - private static final String TOOL_CALLS_JSON_PREFIX = "{\"_btc_tc\":"; - private static final String TOOL_RESPONSES_JSON_PREFIX = "{\"_btc_tr\":"; - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final TypeReference> TOOL_CALL_LIST_TYPE = - new TypeReference<>() {}; - private static final TypeReference> TOOL_RESPONSE_LIST_TYPE = - new TypeReference<>() {}; - private final ChatMemoryRepository delegate; private final JdbcTemplate jdbcTemplate; private final String findConversationIdsOrderedSql; - private final JdbcChatMemoryRepositoryDialect dialect; @SuppressFBWarnings("EI2") public OrderedJdbcChatMemoryRepository( @@ -77,12 +41,6 @@ public OrderedJdbcChatMemoryRepository( this.delegate = delegate; this.jdbcTemplate = jdbcTemplate; this.findConversationIdsOrderedSql = findConversationIdsOrderedSql; - - DataSource dataSource = jdbcTemplate.getDataSource(); - - this.dialect = dataSource != null - ? JdbcChatMemoryRepositoryDialect.from(dataSource) - : new PostgresChatMemoryRepositoryDialect(); } @Override @@ -92,175 +50,17 @@ public List findConversationIds() { } @Override - @SuppressFBWarnings("SQL_INJECTION_SPRING_JDBC") public List findByConversationId(String conversationId) { - List messages = jdbcTemplate.query( - dialect.getSelectMessagesSql(), - (rs, rowNum) -> decodeMessage(rs.getString(1), rs.getString(2)), - conversationId); - - return filterBrokenToolCallSequences(messages); + return delegate.findByConversationId(conversationId); } @Override - @SuppressFBWarnings("SQL_INJECTION_SPRING_JDBC") public void saveAll(String conversationId, List messages) { - delegate.deleteByConversationId(conversationId); - - if (messages.isEmpty()) { - return; - } - - AtomicLong sequenceId = new AtomicLong(Instant.now() - .getEpochSecond()); - - jdbcTemplate.batchUpdate(dialect.getInsertMessageSql(), new BatchPreparedStatementSetter() { - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - Message message = messages.get(i); - - ps.setString(1, conversationId); - ps.setString(2, encodeContent(message)); - ps.setString(3, message.getMessageType() - .name()); - ps.setTimestamp(4, new Timestamp(sequenceId.getAndIncrement() * 1000L)); - } - - @Override - public int getBatchSize() { - return messages.size(); - } - }); + delegate.saveAll(conversationId, messages); } @Override public void deleteByConversationId(String conversationId) { delegate.deleteByConversationId(conversationId); } - - /** - * Filters out broken tool-call sequences that cannot be sent to an LLM. Spring AI's - * {@code JdbcChatMemoryRepository} cannot reconstruct {@code ToolResponseMessage} objects with a valid - * {@code toolCallId} — it always creates empty-responses instances. This causes 400 errors from the LLM API. - *

- * This filter removes: - *

    - *
  • An {@link AssistantMessage} with tool calls followed by one or more empty {@link ToolResponseMessage} objects - * (broken cross-turn sequences from old storage).
  • - *
  • Orphaned empty {@link ToolResponseMessage} objects not preceded by an AssistantMessage with tool calls.
  • - *
- * Within-turn safety: the {@link AssistantMessage} with tool calls is saved before its {@link ToolResponseMessage}, - * so mid-turn reads contain only the former and it is preserved intact. - */ - static List filterBrokenToolCallSequences(List messages) { - List result = new ArrayList<>(messages.size()); - - for (int i = 0; i < messages.size(); i++) { - Message message = messages.get(i); - - if (message instanceof AssistantMessage assistantMessage && assistantMessage.hasToolCalls()) { - int j = i + 1; - - while (j < messages.size() && messages.get(j) instanceof ToolResponseMessage toolResponse - && toolResponse.getResponses() - .isEmpty()) { - j++; - } - - if (j > i + 1) { - i = j - 1; - - continue; - } - } else if (message instanceof ToolResponseMessage toolResponse && toolResponse.getResponses() - .isEmpty()) { - continue; - } - - result.add(message); - } - - return result; - } - - static String encodeContent(Message message) { - if (message instanceof AssistantMessage assistantMessage && assistantMessage.hasToolCalls()) { - try { - return TOOL_CALLS_JSON_PREFIX + OBJECT_MAPPER.writeValueAsString(assistantMessage.getToolCalls()); - } catch (JsonProcessingException e) { - return message.getText() != null ? message.getText() : ""; - } - } - - if (message instanceof ToolResponseMessage toolResponseMessage - && !toolResponseMessage.getResponses() - .isEmpty()) { - try { - return TOOL_RESPONSES_JSON_PREFIX - + OBJECT_MAPPER.writeValueAsString(toolResponseMessage.getResponses()); - } catch (JsonProcessingException e) { - return ""; - } - } - - return message.getText() != null ? message.getText() : ""; - } - - static Message decodeMessage(@Nullable String content, String type) { - MessageType messageType; - - try { - messageType = MessageType.valueOf(type); - } catch (IllegalArgumentException e) { - return new UserMessage(content != null ? content : ""); - } - - return switch (messageType) { - case USER -> new UserMessage(content != null ? content : ""); - case SYSTEM -> new SystemMessage(content != null ? content : ""); - case ASSISTANT -> decodeAssistantMessage(content); - case TOOL -> decodeToolResponseMessage(content); - }; - } - - private static Message decodeAssistantMessage(@Nullable String content) { - if (content != null && content.startsWith(TOOL_CALLS_JSON_PREFIX)) { - try { - List toolCalls = - OBJECT_MAPPER.readValue(content.substring(TOOL_CALLS_JSON_PREFIX.length()), TOOL_CALL_LIST_TYPE); - - return AssistantMessage.builder() - .content("") - .toolCalls(toolCalls) - .build(); - } catch (JsonProcessingException e) { - return new AssistantMessage(content); - } - } - - return new AssistantMessage(content != null ? content : ""); - } - - private static Message decodeToolResponseMessage(@Nullable String content) { - if (content != null && content.startsWith(TOOL_RESPONSES_JSON_PREFIX)) { - try { - List responses = - OBJECT_MAPPER.readValue( - content.substring(TOOL_RESPONSES_JSON_PREFIX.length()), TOOL_RESPONSE_LIST_TYPE); - - return ToolResponseMessage.builder() - .responses(responses) - .build(); - } catch (JsonProcessingException e) { - return ToolResponseMessage.builder() - .responses(List.of()) - .build(); - } - } - - return ToolResponseMessage.builder() - .responses(List.of()) - .build(); - } } diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java index f2c4dc2844c..421d1808957 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-vectorstore/src/main/java/com/bytechef/component/ai/chat/memory/cluster/VectorStoreChatMemory.java @@ -33,11 +33,6 @@ import com.bytechef.platform.configuration.domain.ClusterElement; import com.bytechef.platform.configuration.domain.ClusterElementMap; import java.util.Map; -import java.util.Objects; -import org.springframework.ai.chat.client.ChatClientRequest; -import org.springframework.ai.chat.client.ChatClientResponse; -import org.springframework.ai.chat.client.advisor.api.AdvisorChain; -import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor.Builder; @@ -104,45 +99,6 @@ protected ChatMemoryFunction.Result apply( .defaultTopK( inputParameters.getInteger(CHAT_MEMORY_RETRIEVE_SIZE, 20)); - return new ChatMemoryFunction.Result(new ToolCallSafeVectorStoreChatMemoryAdvisor(builder.build()), null); - } - - /** - * Guards {@link VectorStoreChatMemoryAdvisor} against the second iteration of a {@code ToolCallAdvisor} loop. In - * that iteration the prompt has only a {@code SystemMessage} and a {@code ToolResponseMessage} — no real user - * message — so {@code getUserMessage().getText()} is blank. Passing a blank string to the embedding API causes a - * 400 error. When there is no real user message we skip the vector-store lookup and pass the request through - * unchanged; the {@code after()} delegation is preserved so the final assistant reply is still stored. - */ - private static class ToolCallSafeVectorStoreChatMemoryAdvisor implements BaseChatMemoryAdvisor { - - private final VectorStoreChatMemoryAdvisor delegate; - - ToolCallSafeVectorStoreChatMemoryAdvisor(VectorStoreChatMemoryAdvisor delegate) { - this.delegate = delegate; - } - - @Override - public ChatClientRequest before(ChatClientRequest request, AdvisorChain advisorChain) { - String userText = Objects.requireNonNullElse(request.prompt() - .getUserMessage() - .getText(), ""); - - if (userText.isBlank()) { - return request; - } - - return delegate.before(request, advisorChain); - } - - @Override - public ChatClientResponse after(ChatClientResponse response, AdvisorChain advisorChain) { - return delegate.after(response, advisorChain); - } - - @Override - public int getOrder() { - return delegate.getOrder(); - } + return new ChatMemoryFunction.Result(builder.build(), null); } } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 7d711945755..8b710d60e7f 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -68,7 +68,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.memory.ChatMemory; @@ -361,17 +360,12 @@ List getAdvisors( if (chatMemoryAdvisor != null) { advisors.add(chatMemoryAdvisor); - // Disable ToolCallAdvisor's internal conversation history only when a MessageChatMemoryAdvisor - // is present. That advisor adds previous messages to the prompt directly, so ToolCallAdvisor - // must not duplicate them (double bookkeeping → malformed tool-call sequences on OpenAI). - // - // VectorStoreChatMemoryAdvisor is NOT a MessageChatMemoryAdvisor — it only augments the system - // message via semantic search and does not inject conversation messages into the prompt. With it, - // ToolCallAdvisor must keep its internal history so the second loop iteration includes the - // AssistantMessage(toolCalls) that OpenAI requires before the ToolResponseMessage. - if (chatMemoryAdvisor instanceof MessageChatMemoryAdvisor) { - toolCallAdvisorBuilder.disableInternalConversationHistory(); - } + // Spring AI auto-applies this when ToolCallAdvisor is auto-registered (see DefaultChatClient + // .autoRegisterToolCallAdvisor), but we register manually to supply our own ToolCallingManager, + // so the auto-disable path is skipped and we must call it ourselves. Removing this line makes + // ToolCallAdvisor carry full conversation history alongside the downstream ChatMemoryAdvisor — + // double bookkeeping that produces malformed tool-call sequences on OpenAI. + toolCallAdvisorBuilder.disableInternalConversationHistory(); } advisors.add(toolCallAdvisorBuilder.build()); diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index 6a36d7f1e7a..927f67459f2 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -501,37 +501,17 @@ void testGetAdvisorsIncludesToolCallAdvisorWithDefaultConversationHistoryWhenNoC } @Test - void testGetAdvisorsDisablesConversationHistoryForMessageChatMemoryAdvisor() throws Exception { - ClusterElementMap clusterElementMap = buildClusterElementMapWithMemory(); - - MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor - .builder(mock(ChatMemory.class)) - .build(); - - ChatMemoryFunction chatMemoryFunction = mock(ChatMemoryFunction.class); - - when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(chatMemoryAdvisor); - when(clusterElementDefinitionService.getClusterElement( - eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); - - Map connectionParameters = Map.of( - "memory_1", new ComponentConnection("memoryComponent", 1, 2L, Map.of(), null)); - - List advisors = new TestAiAgentChatAction( - aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) - .getAdvisors(clusterElementMap, connectionParameters, mock(ActionContext.class)); - - int chatMemoryIndex = advisors.indexOf(chatMemoryAdvisor); - ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); + void testGetAdvisorsAddsChatMemoryBeforeToolCallAdvisorAndDisablesInternalConversationHistory() throws Exception { + Map chatMemoryElement = new HashMap<>(); - assertThat(chatMemoryIndex).isGreaterThanOrEqualTo(0); - assertThat(advisors.indexOf(toolCallAdvisor)).isGreaterThan(chatMemoryIndex); - assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isFalse(); - } + chatMemoryElement.put("name", "memory_1"); + chatMemoryElement.put("type", "memoryComponent/v1/memoryElement"); + chatMemoryElement.put("parameters", Map.of()); - @Test - void testGetAdvisorsKeepsConversationHistoryForNonMessageChatMemoryAdvisor() throws Exception { - ClusterElementMap clusterElementMap = buildClusterElementMapWithMemory(); + ClusterElementMap clusterElementMap = ClusterElementMap.of( + Map.of( + "clusterElements", + Map.of("model", buildModelClusterElement(), "chatMemory", chatMemoryElement))); BaseChatMemoryAdvisor chatMemoryAdvisor = mock(BaseChatMemoryAdvisor.class); @@ -541,31 +521,24 @@ void testGetAdvisorsKeepsConversationHistoryForNonMessageChatMemoryAdvisor() thr when(clusterElementDefinitionService.getClusterElement( eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); - Map connectionParameters = Map.of( - "memory_1", new ComponentConnection("memoryComponent", 1, 2L, Map.of(), null)); + ComponentConnection memoryConnection = new ComponentConnection( + "memoryComponent", 1, 2L, Map.of(), null); + + Map connectionParameters = Map.of("memory_1", memoryConnection); + ActionContext actionContext = mock(ActionContext.class); + + TestAiAgentChatAction action = new TestAiAgentChatAction( + aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager); - List advisors = new TestAiAgentChatAction( - aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager) - .getAdvisors(clusterElementMap, connectionParameters, mock(ActionContext.class)); + List advisors = action.getAdvisors(clusterElementMap, connectionParameters, actionContext); int chatMemoryIndex = advisors.indexOf(chatMemoryAdvisor); ToolCallAdvisor toolCallAdvisor = findToolCallAdvisor(advisors); + int toolCallIndex = advisors.indexOf(toolCallAdvisor); assertThat(chatMemoryIndex).isGreaterThanOrEqualTo(0); - assertThat(advisors.indexOf(toolCallAdvisor)).isGreaterThan(chatMemoryIndex); - assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isTrue(); - } - - private static ClusterElementMap buildClusterElementMapWithMemory() { - Map chatMemoryElement = new HashMap<>(); - - chatMemoryElement.put("name", "memory_1"); - chatMemoryElement.put("type", "memoryComponent/v1/memoryElement"); - chatMemoryElement.put("parameters", Map.of()); - - return ClusterElementMap.of( - Map.of("clusterElements", - Map.of("model", buildModelClusterElement(), "chatMemory", chatMemoryElement))); + assertThat(toolCallIndex).isGreaterThan(chatMemoryIndex); + assertThat(readConversationHistoryEnabled(toolCallAdvisor)).isFalse(); } private static Map buildModelClusterElement() { From 059d4e2f0eda853340cc8b710d08f9e1877be62b Mon Sep 17 00:00:00 2001 From: mkriskovic Date: Fri, 5 Jun 2026 13:38:54 +0200 Subject: [PATCH 21/24] 1652 - remembering internal tool conversation history on disableInternalConversationHistory --- .../memory/builtin/cluster/ChatMemory.java | 52 +++++- .../action/AbstractAiAgentChatAction.java | 4 +- .../advisor/ToolHistoryToolCallAdvisor.java | 167 ++++++++++++++++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java diff --git a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java index 73ca369496f..251867adbdc 100644 --- a/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java +++ b/server/libs/modules/components/ai/agent/chat-memory/chat-memory-builtin/src/main/java/com/bytechef/component/ai/agent/chat/memory/builtin/cluster/ChatMemory.java @@ -25,9 +25,13 @@ import com.bytechef.component.definition.ComponentDsl; import com.bytechef.component.definition.Parameters; import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; +import java.util.List; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.ToolResponseMessage; /** * @author Ivica Cardic @@ -61,8 +65,54 @@ protected static ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(chatMemory) + MessageChatMemoryAdvisor.builder(new ToolCallIntermediateMessageFilteringChatMemory(chatMemory)) .build(), chatMemory); } + + /** + * Wraps a {@link org.springframework.ai.chat.memory.ChatMemory} and suppresses intermediate tool-call messages from + * being persisted. Specifically, {@link AssistantMessage} instances that carry tool_calls and + * {@link ToolResponseMessage} instances are skipped on {@code add()} — only user messages and final assistant + * replies are stored. This prevents JDBC-backed memory from accumulating empty/stripped tool-call stubs that would + * otherwise cause 400 errors from LLM providers on subsequent turns. + */ + private static class ToolCallIntermediateMessageFilteringChatMemory + implements org.springframework.ai.chat.memory.ChatMemory { + + private final org.springframework.ai.chat.memory.ChatMemory delegate; + + ToolCallIntermediateMessageFilteringChatMemory(org.springframework.ai.chat.memory.ChatMemory delegate) { + this.delegate = delegate; + } + + @Override + public void add(String conversationId, List messages) { + List filtered = messages.stream() + .filter(ToolCallIntermediateMessageFilteringChatMemory::isStorable) + .toList(); + + if (!filtered.isEmpty()) { + delegate.add(conversationId, filtered); + } + } + + @Override + public List get(String conversationId) { + return delegate.get(conversationId); + } + + @Override + public void clear(String conversationId) { + delegate.clear(conversationId); + } + + private static boolean isStorable(Message message) { + if (message instanceof AssistantMessage assistantMessage) { + return !assistantMessage.hasToolCalls(); + } + + return !(message instanceof ToolResponseMessage); + } + } } diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 8b710d60e7f..0ad23453c9d 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -35,6 +35,7 @@ import com.bytechef.component.ai.agent.facade.AiAgentToolFacade; import com.bytechef.component.ai.llm.ChatModel.ResponseFormat; import com.bytechef.component.ai.llm.advisor.ContextLoggerAdvisor; +import com.bytechef.component.ai.llm.advisor.ToolHistoryToolCallAdvisor; import com.bytechef.component.ai.llm.converter.JsonSchemaStructuredOutputConverter; import com.bytechef.component.ai.llm.util.ModelUtils; import com.bytechef.component.definition.ActionContext; @@ -68,7 +69,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; @@ -348,7 +348,7 @@ List getAdvisors( // tool call - ToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolCallAdvisor.builder() + ToolHistoryToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolHistoryToolCallAdvisor.builder() .toolCallingManager(toolCallingManager); // memory diff --git a/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java b/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java new file mode 100644 index 00000000000..9411e198515 --- /dev/null +++ b/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java @@ -0,0 +1,167 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.llm.advisor; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.ai.chat.client.ChatClientRequest; +import org.springframework.ai.chat.client.ChatClientResponse; +import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.ToolResponseMessage; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.ToolExecutionResult; + +/** + * Extends {@link ToolCallAdvisor} to restore complete in-memory tool-call context in each loop iteration when + * {@link ToolCallAdvisor.Builder#disableInternalConversationHistory()} is active. + * + *

+ * When conversation history is delegated to a downstream {@link org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor}, + * the default implementation passes only {@code [SystemMessage, ToolResponseMessage_last]} as instructions for the next + * iteration and relies on the memory advisor to reconstruct context from the database. However, when the memory store + * is wrapped with {@code ToolCallIntermediateMessageFilteringChatMemory} (which prevents + * {@link AssistantMessage}-with-tool_calls and {@link ToolResponseMessage} from being persisted to keep the database + * clean), the memory advisor can no longer provide the required tool-call context. + * + *

+ * This class overrides {@link #doGetNextInstructionsForToolCall} to extract all + * {@code (AssistantMessage-with-tool_calls, ToolResponseMessage)} pairs directly from the in-memory + * {@link ToolExecutionResult#conversationHistory()} and include them in the instructions. The memory advisor then + * only needs to supply the original user messages (which are still persisted), avoiding both database pollution and + * context gaps during multi-step tool call loops. + * + * @author Marko Kriskovic + */ +public class ToolHistoryToolCallAdvisor extends ToolCallAdvisor { + + private final boolean conversationHistoryEnabled; + + protected ToolHistoryToolCallAdvisor( + ToolCallingManager toolCallingManager, int advisorOrder, boolean conversationHistoryEnabled, + boolean streamToolCallResponses) { + + super(toolCallingManager, advisorOrder, conversationHistoryEnabled, streamToolCallResponses); + + this.conversationHistoryEnabled = conversationHistoryEnabled; + } + + @Override + protected List doGetNextInstructionsForToolCall( + ChatClientRequest chatClientRequest, ChatClientResponse chatClientResponse, + ToolExecutionResult toolExecutionResult) { + + if (conversationHistoryEnabled) { + return super.doGetNextInstructionsForToolCall(chatClientRequest, chatClientResponse, toolExecutionResult); + } + + List toolCallPairs = extractToolCallPairs(toolExecutionResult.conversationHistory()); + + if (toolCallPairs.isEmpty()) { + return super.doGetNextInstructionsForToolCall(chatClientRequest, chatClientResponse, toolExecutionResult); + } + + Message systemMessage = chatClientRequest.prompt() + .getSystemMessage(); + + List instructions = new ArrayList<>(toolCallPairs.size() + 1); + + if (systemMessage != null) { + instructions.add(systemMessage); + } + + instructions.addAll(toolCallPairs); + + return instructions; + } + + @Override + protected List doGetNextInstructionsForToolCallStream( + ChatClientRequest chatClientRequest, ChatClientResponse chatClientResponse, + ToolExecutionResult toolExecutionResult) { + + if (conversationHistoryEnabled) { + return super.doGetNextInstructionsForToolCallStream( + chatClientRequest, chatClientResponse, toolExecutionResult); + } + + List toolCallPairs = extractToolCallPairs(toolExecutionResult.conversationHistory()); + + if (toolCallPairs.isEmpty()) { + return super.doGetNextInstructionsForToolCallStream( + chatClientRequest, chatClientResponse, toolExecutionResult); + } + + Message systemMessage = chatClientRequest.prompt() + .getSystemMessage(); + + List instructions = new ArrayList<>(toolCallPairs.size() + 1); + + if (systemMessage != null) { + instructions.add(systemMessage); + } + + instructions.addAll(toolCallPairs); + + return instructions; + } + + /** + * Extracts all {@code (AssistantMessage-with-tool_calls, ToolResponseMessage)} pairs from a conversation history, + * ignoring user and system messages. Used to reconstruct tool-call context without relying on persisted history. + */ + private static List extractToolCallPairs(List history) { + List pairs = new ArrayList<>(); + + for (int i = 0; i < history.size() - 1; i++) { + if (history.get(i) instanceof AssistantMessage assistantMessage + && assistantMessage.hasToolCalls() + && history.get(i + 1) instanceof ToolResponseMessage toolResponseMessage) { + + pairs.add(assistantMessage); + pairs.add(toolResponseMessage); + + i++; + } + } + + return pairs; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Fluent builder for {@link ToolHistoryToolCallAdvisor}. + */ + public static final class Builder extends ToolCallAdvisor.Builder { + + @Override + protected Builder self() { + return this; + } + + @Override + public ToolHistoryToolCallAdvisor build() { + return new ToolHistoryToolCallAdvisor( + getToolCallingManager(), getAdvisorOrder(), isConversationHistoryEnabled(), + isStreamToolCallResponses()); + } + } +} From 2f3ee5bf5c75809f1a1923400ec21e89f76e7bca Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Sat, 6 Jun 2026 11:43:12 +0200 Subject: [PATCH 22/24] 1652 - Fix test --- .../ai/agent/action/AbstractAiAgentChatActionTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java index 927f67459f2..30501dbb6b8 100644 --- a/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java +++ b/server/libs/modules/components/ai/agent/src/test/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatActionTest.java @@ -408,7 +408,8 @@ void testChatMemoryAdvisorOrderedDownstreamOfToolCallAdvisor() throws Exception .builder(mock(ChatMemory.class)) .build(); - when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(productionStyleChatMemoryAdvisor); + when(chatMemoryFunction.apply(any(), any(), any(), any())) + .thenReturn(new ChatMemoryFunction.Result(productionStyleChatMemoryAdvisor, null)); ComponentConnection componentConnection = new ComponentConnection( "testComponent", 1, 1L, Map.of(), null); @@ -517,7 +518,8 @@ void testGetAdvisorsAddsChatMemoryBeforeToolCallAdvisorAndDisablesInternalConver ChatMemoryFunction chatMemoryFunction = mock(ChatMemoryFunction.class); - when(chatMemoryFunction.apply(any(), any(), any(), any())).thenReturn(chatMemoryAdvisor); + when(chatMemoryFunction.apply(any(), any(), any(), any())) + .thenReturn(new ChatMemoryFunction.Result(chatMemoryAdvisor, null)); when(clusterElementDefinitionService.getClusterElement( eq("memoryComponent"), eq(1), eq("memoryElement"))).thenReturn(chatMemoryFunction); From 7f151436f2932ed7928a3043f6ce6b9422efb2fc Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Sat, 6 Jun 2026 11:43:18 +0200 Subject: [PATCH 23/24] 4096 client - SF --- .../automation/project/hooks/useConverterN8nToWorkflow.ts | 7 +++---- .../automation/project/utils/handleImportN8nWorkflow.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/pages/automation/project/hooks/useConverterN8nToWorkflow.ts b/client/src/pages/automation/project/hooks/useConverterN8nToWorkflow.ts index 93404b9aa0d..472fbd62760 100644 --- a/client/src/pages/automation/project/hooks/useConverterN8nToWorkflow.ts +++ b/client/src/pages/automation/project/hooks/useConverterN8nToWorkflow.ts @@ -11,7 +11,6 @@ export const useConvertN8nToWorkflow = () => { const setWorkflow = useWorkflowDataStore((state) => state.setWorkflow); const [isRunning, setIsRunning] = useState(false); - const convertN8nWorkflow = useCallback( async (workflowJson: string) => { const json = JSON.parse(workflowJson); @@ -51,9 +50,9 @@ export const useConvertN8nToWorkflow = () => { } catch (e) { console.error('Failed to parse workflow', e); - throw new Error( - 'The n8n workflow could not be converted into a valid ByteChef workflow.' - ); + throw new Error('The n8n workflow could not be converted into a valid ByteChef workflow.', { + cause: e, + }); } finally { setIsRunning(false); } diff --git a/client/src/pages/automation/project/utils/handleImportN8nWorkflow.ts b/client/src/pages/automation/project/utils/handleImportN8nWorkflow.ts index c99c6b0fbea..06b76a70142 100644 --- a/client/src/pages/automation/project/utils/handleImportN8nWorkflow.ts +++ b/client/src/pages/automation/project/utils/handleImportN8nWorkflow.ts @@ -4,7 +4,7 @@ import { } from '@/shared/middleware/automation/configuration'; import {UseMutationResult} from '@tanstack/react-query'; import {ChangeEvent} from 'react'; -import {toast} from "sonner"; +import {toast} from 'sonner'; const handleImportN8nWorkflow = async ( event: ChangeEvent, From cbf9250ef2bf51cfd0dd32b3b3c930d172a5624c Mon Sep 17 00:00:00 2001 From: Ivica Cardic Date: Sat, 6 Jun 2026 11:47:43 +0200 Subject: [PATCH 24/24] 1652 SF --- .../advisor/ToolHistoryToolCallAdvisor.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java b/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java index 9411e198515..f7538797222 100644 --- a/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java +++ b/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java @@ -32,19 +32,20 @@ * {@link ToolCallAdvisor.Builder#disableInternalConversationHistory()} is active. * *

- * When conversation history is delegated to a downstream {@link org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor}, - * the default implementation passes only {@code [SystemMessage, ToolResponseMessage_last]} as instructions for the next - * iteration and relies on the memory advisor to reconstruct context from the database. However, when the memory store - * is wrapped with {@code ToolCallIntermediateMessageFilteringChatMemory} (which prevents - * {@link AssistantMessage}-with-tool_calls and {@link ToolResponseMessage} from being persisted to keep the database - * clean), the memory advisor can no longer provide the required tool-call context. + * When conversation history is delegated to a downstream + * {@link org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor}, the default implementation passes only + * {@code [SystemMessage, ToolResponseMessage_last]} as instructions for the next iteration and relies on the memory + * advisor to reconstruct context from the database. However, when the memory store is wrapped with + * {@code ToolCallIntermediateMessageFilteringChatMemory} (which prevents {@link AssistantMessage}-with-tool_calls and + * {@link ToolResponseMessage} from being persisted to keep the database clean), the memory advisor can no longer + * provide the required tool-call context. * *

* This class overrides {@link #doGetNextInstructionsForToolCall} to extract all * {@code (AssistantMessage-with-tool_calls, ToolResponseMessage)} pairs directly from the in-memory - * {@link ToolExecutionResult#conversationHistory()} and include them in the instructions. The memory advisor then - * only needs to supply the original user messages (which are still persisted), avoiding both database pollution and - * context gaps during multi-step tool call loops. + * {@link ToolExecutionResult#conversationHistory()} and include them in the instructions. The memory advisor then only + * needs to supply the original user messages (which are still persisted), avoiding both database pollution and context + * gaps during multi-step tool call loops. * * @author Marko Kriskovic */ @@ -79,15 +80,15 @@ protected List doGetNextInstructionsForToolCall( Message systemMessage = chatClientRequest.prompt() .getSystemMessage(); - List instructions = new ArrayList<>(toolCallPairs.size() + 1); + List instructionMessages = new ArrayList<>(toolCallPairs.size() + 1); if (systemMessage != null) { - instructions.add(systemMessage); + instructionMessages.add(systemMessage); } - instructions.addAll(toolCallPairs); + instructionMessages.addAll(toolCallPairs); - return instructions; + return instructionMessages; } @Override @@ -125,22 +126,22 @@ protected List doGetNextInstructionsForToolCallStream( * Extracts all {@code (AssistantMessage-with-tool_calls, ToolResponseMessage)} pairs from a conversation history, * ignoring user and system messages. Used to reconstruct tool-call context without relying on persisted history. */ - private static List extractToolCallPairs(List history) { - List pairs = new ArrayList<>(); + private static List extractToolCallPairs(List conversationHistoryMessages) { + List toolCallPairMessages = new ArrayList<>(); - for (int i = 0; i < history.size() - 1; i++) { - if (history.get(i) instanceof AssistantMessage assistantMessage - && assistantMessage.hasToolCalls() - && history.get(i + 1) instanceof ToolResponseMessage toolResponseMessage) { + for (int i = 0; i < conversationHistoryMessages.size() - 1; i++) { + if (conversationHistoryMessages.get(i) instanceof AssistantMessage assistantMessage && + assistantMessage.hasToolCalls() && + conversationHistoryMessages.get(i + 1) instanceof ToolResponseMessage toolResponseMessage) { - pairs.add(assistantMessage); - pairs.add(toolResponseMessage); + toolCallPairMessages.add(assistantMessage); + toolCallPairMessages.add(toolResponseMessage); i++; } } - return pairs; + return toolCallPairMessages; } public static Builder builder() {