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, 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..7d595573c94 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" @@ -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-M8" spring-boot = "4.0.6" spring-cloud-aws = "4.0.2" spring-cloud-dependencies = "2025.1.1" 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/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 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/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/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/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/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java similarity index 57% rename from server/libs/modules/components/ai/agent/chat-memory/chat-memory-cosmosdb/src/test/java/com/bytechef/component/ai/agent/chat/memory/cosmosdb/CosmosDbChatMemoryComponentHandlerTest.java rename to server/libs/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java index f47aa0138c7..9bcfaf18a45 100644 --- 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/ai/mcp/mcp-tool/mcp-tool-platform/src/main/java/com/bytechef/ai/mcp/tool/platform/exception/WorkflowValidatorToolErrorType.java @@ -14,16 +14,18 @@ * limitations under the License. */ -package com.bytechef.component.ai.agent.chat.memory.cosmosdb; +package com.bytechef.ai.mcp.tool.platform.exception; -import com.bytechef.test.jsonasssert.JsonFileAssert; -import org.junit.jupiter.api.Test; +import com.bytechef.exception.AbstractErrorType; -public class CosmosDbChatMemoryComponentHandlerTest { +/** + * @author Marko Kriskovic + */ +public class WorkflowValidatorToolErrorType extends AbstractErrorType { + + public static final WorkflowValidatorToolErrorType VALIDATE_WORKFLOW = new WorkflowValidatorToolErrorType(100); - @Test - public void testGetComponentDefinition() { - JsonFileAssert.assertEquals( - "definition/cosmos-db-chat-memory_v1.json", new CosmosDbChatMemoryComponentHandler().getDefinition()); + 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 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..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,10 +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.client.advisor.api.BaseAdvisor; 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 @@ -62,9 +65,54 @@ protected static ChatMemoryFunction.Result apply( .build(); return new ChatMemoryFunction.Result( - MessageChatMemoryAdvisor.builder(chatMemory) - .order(BaseAdvisor.HIGHEST_PRECEDENCE + 200) + 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/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-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/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/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/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..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 @@ -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(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 6688202ba74..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; @@ -74,11 +75,12 @@ 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; -import org.springframework.ai.util.json.JsonParser; import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; /** * @author Ivica Cardic @@ -86,17 +88,21 @@ 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]"; 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( @@ -147,10 +153,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( @@ -216,7 +222,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( @@ -297,7 +303,7 @@ public String call(String toolInput, @Nullable ToolContext toolContext) { }; } - private List getAdvisors( + List getAdvisors( ClusterElementMap clusterElementMap, Map connectionParameters, ActionContext context) { @@ -340,8 +346,29 @@ private List getAdvisors( } } - chatMemoryResult.map(ChatMemoryFunction.Result::advisor) - .ifPresent(advisors::add); + // tool call + + ToolHistoryToolCallAdvisor.Builder toolCallAdvisorBuilder = ToolHistoryToolCallAdvisor.builder() + .toolCallingManager(toolCallingManager); + + // memory + + Advisor chatMemoryAdvisor = chatMemoryResult + .map(ChatMemoryFunction.Result::advisor) + .orElse(null); + + 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(); + } + + 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..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 @@ -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(aiAgentToolFacade, clusterElementDefinitionService, toolCallingManager).build(); } - private AiAgentChatAction(ClusterElementDefinitionService clusterElementDefinitionService, - AiAgentToolFacade aiAgentToolFacade) { - super(clusterElementDefinitionService, aiAgentToolFacade); + private AiAgentChatAction( + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + 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..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 @@ -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) { + AiAgentToolFacade aiAgentToolFacade, ClusterElementDefinitionService clusterElementDefinitionService, + 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..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 @@ -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,8 +49,15 @@ 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; /** * @author Ivica Cardic @@ -56,11 +65,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 +125,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())) @@ -148,7 +161,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())) @@ -185,7 +199,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())) @@ -224,8 +239,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( @@ -248,7 +262,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())) @@ -300,7 +315,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())) @@ -336,7 +352,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())) @@ -354,6 +371,105 @@ 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(new ChatMemoryFunction.Result(productionStyleChatMemoryAdvisor, null)); + + 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"); @@ -366,6 +482,77 @@ 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(new ChatMemoryFunction.Result(chatMemoryAdvisor, null)); + 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); @@ -384,11 +571,36 @@ 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(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()); } } 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, 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..f7538797222 --- /dev/null +++ b/server/libs/modules/components/ai/llm/src/main/java/com/bytechef/component/ai/llm/advisor/ToolHistoryToolCallAdvisor.java @@ -0,0 +1,168 @@ +/* + * 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 instructionMessages = new ArrayList<>(toolCallPairs.size() + 1); + + if (systemMessage != null) { + instructionMessages.add(systemMessage); + } + + instructionMessages.addAll(toolCallPairs); + + return instructionMessages; + } + + @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 conversationHistoryMessages) { + List toolCallPairMessages = new ArrayList<>(); + + 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) { + + toolCallPairMessages.add(assistantMessage); + toolCallPairMessages.add(toolResponseMessage); + + i++; + } + } + + return toolCallPairMessages; + } + + 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()); + } + } +} 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/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()); }; } 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")