From 4a0a2a775bfed8c2afd53455cb0a2692eebd89ed Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 22 Jul 2025 14:41:18 -0700 Subject: [PATCH 1/6] initial --- experimental/ai/impl/pom.xml | 58 +++ .../executors/ai/AIChatModelCallExecutor.java | 172 +++++++ .../ai/AIChatModelTaskExecutorFactory.java | 21 + .../impl/services/ChatModelService.java | 25 + ...erlessworkflow.impl.executors.CallableTask | 1 + ...orkflow.impl.executors.TaskExecutorFactory | 1 + experimental/ai/models/openai/pom.xml | 27 ++ .../openai/OpenAIChatModelService.java | 96 ++++ ...essworkflow.impl.services.ChatModelService | 1 + experimental/ai/models/pom.xml | 19 + experimental/ai/pom.xml | 17 + experimental/pom.xml | 1 + .../api/types/ai/CallAIChatModel.java | 440 ++++++++++++++++++ .../api/types/ai/CallTaskAIChatModel.java | 37 ++ 14 files changed, 916 insertions(+) create mode 100644 experimental/ai/impl/pom.xml create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelTaskExecutorFactory.java create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java create mode 100644 experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask create mode 100644 experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory create mode 100644 experimental/ai/models/openai/pom.xml create mode 100644 experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java create mode 100644 experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService create mode 100644 experimental/ai/models/pom.xml create mode 100644 experimental/ai/pom.xml create mode 100644 experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java create mode 100644 experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java diff --git a/experimental/ai/impl/pom.xml b/experimental/ai/impl/pom.xml new file mode 100644 index 00000000..4953eb75 --- /dev/null +++ b/experimental/ai/impl/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-experimental-ai-parent + 8.0.0-SNAPSHOT + + serverlessworkflow-experimental-ai-impl + ServelessWorkflow:: Experimental:: AI:: Impl + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + dev.langchain4j + langchain4j + 1.1.0 + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + ch.qos.logback + logback-classic + test + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + \ No newline at end of file diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java new file mode 100644 index 00000000..f969e2e9 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.executors.ai; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.model.output.TokenUsage; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.ai.CallAIChatModel; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowModelFactory; +import io.serverlessworkflow.impl.executors.CallableTask; +import io.serverlessworkflow.impl.resources.ResourceLoader; +import io.serverlessworkflow.impl.services.ChatModelService; + + +public class AIChatModelCallExecutor implements CallableTask { + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*(.+?)\\s*\\}\\}"); + + @Override + public void init(CallAIChatModel task, WorkflowApplication application, ResourceLoader loader) { + + } + + @Override + public CompletableFuture apply(WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { + WorkflowModelFactory modelFactory = workflowContext.definition().application().modelFactory(); + if (taskContext.task() instanceof CallAIChatModel callAIChatModel) { + return CompletableFuture.completedFuture(modelFactory.fromAny(doCall(callAIChatModel, input.asJavaObject()))); + } + throw new IllegalArgumentException("AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " + taskContext.task().getClass().getName()); + } + + @Override + public boolean accept(Class clazz) { + return CallAIChatModel.class.isAssignableFrom(clazz); + } + + private Object doCall(CallAIChatModel callAIChatModel, Object javaObject) { + validate(callAIChatModel, javaObject); + ChatModel chatModel = createChatModel(callAIChatModel); + Map substitutions = (Map) javaObject; + + List messages = new ArrayList<>(); + + if (callAIChatModel.getChatModelRequest().getSystemMessages() != null) { + for (String systemMessage : callAIChatModel.getChatModelRequest().getSystemMessages()) { + String fixedUserMessage = replaceVariables(systemMessage, substitutions); + messages.add(new SystemMessage(fixedUserMessage)); + } + } + + if (callAIChatModel.getChatModelRequest().getUserMessages() != null) { + for (String userMessage : callAIChatModel.getChatModelRequest().getUserMessages()) { + String fixedUserMessage = replaceVariables(userMessage, substitutions); + messages.add(new UserMessage(fixedUserMessage)); + } + } + + return prepareResponse(chatModel.chat(messages), javaObject); + } + + private String replaceVariables(String template, Map substitutions) { + Set variables = extractVariables(template); + for (Map.Entry entry : substitutions.entrySet()) { + String variable = entry.getKey(); + Object value = entry.getValue(); + if (value != null && variables.contains(variable)) { + template = template.replace("{{" + variable + "}}", value.toString()); + } + } + return template; + } + + private void validate(CallAIChatModel callAIChatModel, Object javaObject) { + // TODO + } + + private ChatModel createChatModel(CallAIChatModel callAIChatModel) { + ChatModelService chatModelService = getAvailableModel(); + if (chatModelService != null) { + return chatModelService.getChatModel(callAIChatModel.getPreferences()); + } + throw new IllegalStateException("No LLM models found. Please ensure that you have the required dependencies in your classpath."); + } + + private ChatModelService getAvailableModel() { + ServiceLoader loader = ServiceLoader.load(ChatModelService.class); + + for (ChatModelService service : loader) { + return service; + } + + throw new IllegalStateException("No LLM models found. Please ensure that you have the required dependencies in your classpath."); + } + + private Map prepareResponse(ChatResponse response, Object javaObject) { + + String id = response.id(); + String modelName = response.modelName(); + TokenUsage tokenUsage = response.tokenUsage(); + FinishReason finishReason = response.finishReason(); + AiMessage aiMessage = response.aiMessage(); + + Map responseMap = (Map) javaObject; + if (response.id() != null) { + responseMap.put("id", id); + } + + if (modelName != null) { + responseMap.put("modelName", modelName); + } + + if (tokenUsage != null) { + responseMap.put("tokenUsage.inputTokenCount", tokenUsage.inputTokenCount()); + responseMap.put("tokenUsage.outputTokenCount", tokenUsage.outputTokenCount()); + responseMap.put("tokenUsage.totalTokenCount", tokenUsage.totalTokenCount()); + } + + if (finishReason != null) { + responseMap.put("finishReason", finishReason.name()); + } + + if (aiMessage != null) { + responseMap.put("text", aiMessage.text()); + } + + return responseMap; + } + + private static Set extractVariables(String template) { + Set variables = new HashSet<>(); + Matcher matcher = VARIABLE_PATTERN.matcher(template); + while (matcher.find()) { + variables.add(matcher.group(1)); + } + return variables; + } +} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelTaskExecutorFactory.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelTaskExecutorFactory.java new file mode 100644 index 00000000..c9f78360 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelTaskExecutorFactory.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.executors.ai; + +import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; + +public class AIChatModelTaskExecutorFactory extends DefaultTaskExecutorFactory {} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java new file mode 100644 index 00000000..ddbc4be2 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.services; + +import dev.langchain4j.model.chat.ChatModel; +import io.serverlessworkflow.api.types.ai.CallAIChatModel; + +public interface ChatModelService { + + ChatModel getChatModel(CallAIChatModel.ChatModelPreferences chatModelPreferences); +} diff --git a/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask b/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask new file mode 100644 index 00000000..680fce77 --- /dev/null +++ b/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask @@ -0,0 +1 @@ +io.serverlessworkflow.impl.executors.ai.AIChatModelCallExecutor \ No newline at end of file diff --git a/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory b/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory new file mode 100644 index 00000000..a370284d --- /dev/null +++ b/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory @@ -0,0 +1 @@ +io.serverlessworkflow.impl.executors.ai.AIChatModelTaskExecutorFactory \ No newline at end of file diff --git a/experimental/ai/models/openai/pom.xml b/experimental/ai/models/openai/pom.xml new file mode 100644 index 00000000..42135bfd --- /dev/null +++ b/experimental/ai/models/openai/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-experimental-ai-models-parent + 8.0.0-SNAPSHOT + + serverlessworkflow-experimental-ai-models-openai + ServelessWorkflow:: Experimental:: AI:: Models:: OpenAI + + + + dev.langchain4j + langchain4j-open-ai + 1.1.0 + + + io.serverlessworkflow + serverlessworkflow-experimental-ai-impl + 8.0.0-SNAPSHOT + compile + + + \ No newline at end of file diff --git a/experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java b/experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java new file mode 100644 index 00000000..b05eef89 --- /dev/null +++ b/experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.services.openai; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import io.serverlessworkflow.api.types.ai.CallAIChatModel; +import io.serverlessworkflow.impl.services.ChatModelService; + +public class OpenAIChatModelService implements ChatModelService { + + @Override + public ChatModel getChatModel(CallAIChatModel.ChatModelPreferences chatModelPreferences) { + OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder(); + if (chatModelPreferences.getApiKey() != null) { + builder.apiKey(chatModelPreferences.getApiKey()); + } + if (chatModelPreferences.getModelName() != null) { + builder.modelName(chatModelPreferences.getModelName()); + } + if (chatModelPreferences.getBaseUrl() != null) { + builder.baseUrl(chatModelPreferences.getBaseUrl()); + } + if (chatModelPreferences.getMaxTokens() != null) { + builder.maxTokens(chatModelPreferences.getMaxTokens()); + } + if (chatModelPreferences.getTemperature() != null) { + builder.temperature(chatModelPreferences.getTemperature()); + } + if (chatModelPreferences.getTopP() != null) { + builder.topP(chatModelPreferences.getTopP()); + } + if (chatModelPreferences.getResponseFormat() != null) { + builder.responseFormat(chatModelPreferences.getResponseFormat()); + } + if (chatModelPreferences.getMaxRetries() != null) { + builder.maxRetries(chatModelPreferences.getMaxRetries()); + } + if (chatModelPreferences.getTimeout() != null) { + builder.timeout(chatModelPreferences.getTimeout()); + } + if (chatModelPreferences.getLogRequests() != null) { + builder.logRequests(chatModelPreferences.getLogRequests()); + } + if (chatModelPreferences.getLogResponses() != null) { + builder.logResponses(chatModelPreferences.getLogResponses()); + } + if (chatModelPreferences.getResponseFormat() != null) { + builder.responseFormat(chatModelPreferences.getResponseFormat()); + } + + if (chatModelPreferences.getMaxCompletionTokens() != null) { + builder.maxCompletionTokens(chatModelPreferences.getMaxCompletionTokens()); + } + + if (chatModelPreferences.getPresencePenalty() != null) { + builder.presencePenalty(chatModelPreferences.getPresencePenalty()); + } + + if (chatModelPreferences.getFrequencyPenalty() != null) { + builder.frequencyPenalty(chatModelPreferences.getFrequencyPenalty()); + } + + if (chatModelPreferences.getStrictJsonSchema() != null) { + builder.strictJsonSchema(chatModelPreferences.getStrictJsonSchema()); + } + + if (chatModelPreferences.getSeed() != null) { + builder.seed(chatModelPreferences.getSeed()); + } + + if (chatModelPreferences.getUser() != null) { + builder.user(chatModelPreferences.getUser()); + } + + if (chatModelPreferences.getProjectId() != null) { + builder.projectId(chatModelPreferences.getProjectId()); + } + + return builder.build(); + } +} diff --git a/experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService b/experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService new file mode 100644 index 00000000..7f1d56b1 --- /dev/null +++ b/experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService @@ -0,0 +1 @@ +io.serverlessworkflow.impl.services.openai.OpenAIChatModelService \ No newline at end of file diff --git a/experimental/ai/models/pom.xml b/experimental/ai/models/pom.xml new file mode 100644 index 00000000..b31fc70d --- /dev/null +++ b/experimental/ai/models/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-experimental-ai-parent + 8.0.0-SNAPSHOT + + serverlessworkflow-experimental-ai-models-parent + ServelessWorkflow:: Experimental:: AI:: Models:: Parent + pom + + openai + + + + \ No newline at end of file diff --git a/experimental/ai/pom.xml b/experimental/ai/pom.xml new file mode 100644 index 00000000..9d774798 --- /dev/null +++ b/experimental/ai/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-experimental + 8.0.0-SNAPSHOT + + serverlessworkflow-experimental-ai-parent + ServelessWorkflow:: Experimental:: AI:: Parent + pom + + impl + + \ No newline at end of file diff --git a/experimental/pom.xml b/experimental/pom.xml index 3312207e..84964c61 100644 --- a/experimental/pom.xml +++ b/experimental/pom.xml @@ -40,5 +40,6 @@ types lambda + ai \ No newline at end of file diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java new file mode 100644 index 00000000..61d0e619 --- /dev/null +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java @@ -0,0 +1,440 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.api.types.ai; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.serverlessworkflow.api.types.TaskBase; + +public class CallAIChatModel extends TaskBase { + + private ChatModelPreferences chatModelPreferences; + + private ChatModelRequest chatModelRequest; + + private CallAIChatModel() {} + + public static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return "CallAIChatModel{" + + "chatModelPreferences=" + + chatModelPreferences + + ", chatModelRequest=" + + chatModelRequest + + '}'; + } + + public ChatModelPreferences getPreferences() { + return chatModelPreferences; + } + + public ChatModelRequest getChatModelRequest() { + return chatModelRequest; + } + + public static class Builder { + + private Builder() {} + + private ChatModelPreferences chatModelPreferences; + private ChatModelRequest chatModelRequest; + + public Builder preferences(ChatModelPreferences chatModelPreferences) { + this.chatModelPreferences = chatModelPreferences; + return this; + } + + public Builder request(ChatModelRequest chatModelRequest) { + this.chatModelRequest = chatModelRequest; + return this; + } + + public CallAIChatModel build() { + CallAIChatModel callAIChatModel = new CallAIChatModel(); + callAIChatModel.chatModelPreferences = this.chatModelPreferences; + callAIChatModel.chatModelRequest = this.chatModelRequest; + return callAIChatModel; + } + } + + public static class ChatModelPreferences { + private String baseUrl; + private String apiKey; + private String organizationId; + private String projectId; + private String modelName; + private Double temperature; + private Double topP; + private Integer maxTokens; + private Integer maxCompletionTokens; + private Double presencePenalty; + private Double frequencyPenalty; + private String responseFormat; + private Boolean strictJsonSchema; + private Integer seed; + private String user; + private Duration timeout; + private Integer maxRetries; + private Boolean logRequests; + private Boolean logResponses; + + private ChatModelPreferences() {} + + public static ChatModelPreferences.Builder builder() { + return new ChatModelPreferences.Builder(); + } + + public String getBaseUrl() { + return baseUrl; + } + + public String getApiKey() { + return apiKey; + } + + public String getOrganizationId() { + return organizationId; + } + + public String getProjectId() { + return projectId; + } + + public String getModelName() { + return modelName; + } + + public Double getTemperature() { + return temperature; + } + + public Double getTopP() { + return topP; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public Integer getMaxCompletionTokens() { + return maxCompletionTokens; + } + + public Double getPresencePenalty() { + return presencePenalty; + } + + public Double getFrequencyPenalty() { + return frequencyPenalty; + } + + public String getResponseFormat() { + return responseFormat; + } + + public Boolean getStrictJsonSchema() { + return strictJsonSchema; + } + + public Integer getSeed() { + return seed; + } + + public String getUser() { + return user; + } + + public Duration getTimeout() { + return timeout; + } + + public Integer getMaxRetries() { + return maxRetries; + } + + public Boolean getLogRequests() { + return logRequests; + } + + public Boolean getLogResponses() { + return logResponses; + } + + @Override + public String toString() { + return "Builder{" + + "baseUrl='" + + baseUrl + + '\'' + + ", apiKey='" + + apiKey + + '\'' + + ", organizationId='" + + organizationId + + '\'' + + ", projectId='" + + projectId + + '\'' + + ", modelName='" + + modelName + + '\'' + + ", temperature=" + + temperature + + ", topP=" + + topP + + ", maxTokens=" + + maxTokens + + ", maxCompletionTokens=" + + maxCompletionTokens + + ", presencePenalty=" + + presencePenalty + + ", frequencyPenalty=" + + frequencyPenalty + + ", responseFormat='" + + responseFormat + + '\'' + + ", strictJsonSchema=" + + strictJsonSchema + + ", seed=" + + seed + + ", user='" + + user + + '\'' + + ", timeout=" + + timeout + + ", maxRetries=" + + maxRetries + + ", logRequests=" + + logRequests + + ", logResponses=" + + logResponses + + '}'; + } + + public static class Builder { + private String baseUrl; + private String apiKey; + private String organizationId; + private String projectId; + private String modelName; + private Double temperature; + private Double topP; + private Integer maxTokens; + private Integer maxCompletionTokens; + private Double presencePenalty; + private Double frequencyPenalty; + private String responseFormat; + private Boolean strictJsonSchema; + private Integer seed; + private String user; + private Duration timeout; + private Integer maxRetries; + private Boolean logRequests; + private Boolean logResponses; + + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder organizationId(String organizationId) { + this.organizationId = organizationId; + return this; + } + + public Builder projectId(String projectId) { + this.projectId = projectId; + return this; + } + + public Builder modelName(String modelName) { + this.modelName = modelName; + return this; + } + + public Builder temperature(Double temperature) { + this.temperature = temperature; + return this; + } + + public Builder topP(Double topP) { + this.topP = topP; + return this; + } + + public Builder maxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + return this; + } + + public Builder maxCompletionTokens(Integer maxCompletionTokens) { + this.maxCompletionTokens = maxCompletionTokens; + return this; + } + + public Builder presencePenalty(Double presencePenalty) { + this.presencePenalty = presencePenalty; + return this; + } + + public Builder frequencyPenalty(Double frequencyPenalty) { + this.frequencyPenalty = frequencyPenalty; + return this; + } + + public Builder responseFormat(String responseFormat) { + this.responseFormat = responseFormat; + return this; + } + + public Builder strictJsonSchema(Boolean strictJsonSchema) { + this.strictJsonSchema = strictJsonSchema; + return this; + } + + public Builder seed(Integer seed) { + this.seed = seed; + return this; + } + + public Builder user(String user) { + this.user = user; + return this; + } + + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder logRequests(Boolean logRequests) { + this.logRequests = logRequests; + return this; + } + + public Builder logResponses(Boolean logResponses) { + this.logResponses = logResponses; + return this; + } + + public ChatModelPreferences build() { + ChatModelPreferences preferences = new ChatModelPreferences(); + preferences.baseUrl = this.baseUrl; + preferences.apiKey = this.apiKey; + preferences.organizationId = this.organizationId; + preferences.projectId = this.projectId; + preferences.modelName = this.modelName; + preferences.temperature = this.temperature; + preferences.topP = this.topP; + preferences.maxTokens = this.maxTokens; + preferences.maxCompletionTokens = this.maxCompletionTokens; + preferences.presencePenalty = this.presencePenalty; + preferences.frequencyPenalty = this.frequencyPenalty; + preferences.responseFormat = this.responseFormat; + preferences.strictJsonSchema = this.strictJsonSchema; + preferences.seed = this.seed; + preferences.user = this.user; + preferences.timeout = this.timeout; + preferences.maxRetries = this.maxRetries; + preferences.logRequests = this.logRequests; + preferences.logResponses = this.logResponses; + return preferences; + } + } + } + + public static class ChatModelRequest { + + private List userMessages; + private List systemMessages; + + private ChatModelRequest() {} + + public List getUserMessages() { + return userMessages; + } + + public List getSystemMessages() { + return systemMessages; + } + + public static ChatModelRequest.Builder builder() { + return new ChatModelRequest.Builder(); + } + + @Override + public String toString() { + return "ChatModelRequest{" + + "userMessages=" + + String.join(",", userMessages) + + ", systemMessages=" + + String.join(",", systemMessages) + + '}'; + } + + public static class Builder { + private List userMessages = new ArrayList<>(); + private List systemMessages = new ArrayList<>(); + + private Builder() {} + + public Builder userMessage(String userMessage) { + this.userMessages.add(userMessage); + return this; + } + + public Builder userMessages(Collection userMessages) { + this.userMessages.addAll(userMessages); + return this; + } + + public Builder systemMessage(String systemMessage) { + this.systemMessages.add(systemMessage); + return this; + } + + public Builder systemMessages(Collection systemMessages) { + this.systemMessages.addAll(systemMessages); + return this; + } + + public ChatModelRequest build() { + ChatModelRequest request = new ChatModelRequest(); + request.userMessages = this.userMessages; + request.systemMessages = this.systemMessages; + return request; + } + } + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java new file mode 100644 index 00000000..e64fff12 --- /dev/null +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.api.types.ai; + +import io.serverlessworkflow.api.types.CallTask; + +public class CallTaskAIChatModel extends CallTask { + + private CallAIChatModel callAIChatModel; + + public CallTaskAIChatModel(CallAIChatModel callAIChatModel) { + this.callAIChatModel = callAIChatModel; + } + + public CallAIChatModel getCallAIChatModel() { + return callAIChatModel; + } + + @Override + public Object get() { + return callAIChatModel != null ? callAIChatModel : super.get(); + } +} From a9d180501b7434007a0fda31b9815b53187096e7 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 22 Jul 2025 19:28:07 -0700 Subject: [PATCH 2/6] langchain support task --- experimental/ai/impl/pom.xml | 6 ++ .../executors/ai/AIChatModelCallExecutor.java | 51 ++++++++++------ experimental/ai/pom.xml | 2 + experimental/ai/types/pom.xml | 60 +++++++++++++++++++ .../api/types/CallAILangChainChatModel.java | 39 ++++++++++++ .../api/types/ai/CallAIChatModel.java | 5 +- 6 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 experimental/ai/types/pom.xml create mode 100644 experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java diff --git a/experimental/ai/impl/pom.xml b/experimental/ai/impl/pom.xml index 4953eb75..2f7c9521 100644 --- a/experimental/ai/impl/pom.xml +++ b/experimental/ai/impl/pom.xml @@ -54,5 +54,11 @@ io.serverlessworkflow serverlessworkflow-experimental-types + + io.serverlessworkflow + serverlessworkflow-experimental-ai-types + 8.0.0-SNAPSHOT + compile + \ No newline at end of file diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java index f969e2e9..5b3f48df 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java @@ -16,16 +16,6 @@ package io.serverlessworkflow.impl.executors.ai; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; @@ -34,6 +24,7 @@ import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.output.FinishReason; import dev.langchain4j.model.output.TokenUsage; +import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.ai.CallAIChatModel; import io.serverlessworkflow.impl.TaskContext; @@ -44,24 +35,39 @@ import io.serverlessworkflow.impl.executors.CallableTask; import io.serverlessworkflow.impl.resources.ResourceLoader; import io.serverlessworkflow.impl.services.ChatModelService; - +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class AIChatModelCallExecutor implements CallableTask { private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*(.+?)\\s*\\}\\}"); @Override - public void init(CallAIChatModel task, WorkflowApplication application, ResourceLoader loader) { - - } + public void init(CallAIChatModel task, WorkflowApplication application, ResourceLoader loader) {} @Override - public CompletableFuture apply(WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { + public CompletableFuture apply( + WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { WorkflowModelFactory modelFactory = workflowContext.definition().application().modelFactory(); + if (taskContext.task() instanceof CallAILangChainChatModel callAILangChainChatModel) { + return CompletableFuture.completedFuture( + modelFactory.fromAny(doCall(callAILangChainChatModel, input.asJavaObject()))); + } + if (taskContext.task() instanceof CallAIChatModel callAIChatModel) { - return CompletableFuture.completedFuture(modelFactory.fromAny(doCall(callAIChatModel, input.asJavaObject()))); + return CompletableFuture.completedFuture( + modelFactory.fromAny(doCall(callAIChatModel, input.asJavaObject()))); } - throw new IllegalArgumentException("AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " + taskContext.task().getClass().getName()); + throw new IllegalArgumentException( + "AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " + + taskContext.task().getClass().getName()); } @Override @@ -69,6 +75,11 @@ public boolean accept(Class clazz) { return CallAIChatModel.class.isAssignableFrom(clazz); } + private Object doCall(CallAILangChainChatModel callAIChatModel, Object javaObject) { + ChatModel chatModel = callAIChatModel.getChatModel(); + Class chatModelRequest = callAIChatModel.getChatModelRequest(); + } + private Object doCall(CallAIChatModel callAIChatModel, Object javaObject) { validate(callAIChatModel, javaObject); ChatModel chatModel = createChatModel(callAIChatModel); @@ -114,7 +125,8 @@ private ChatModel createChatModel(CallAIChatModel callAIChatModel) { if (chatModelService != null) { return chatModelService.getChatModel(callAIChatModel.getPreferences()); } - throw new IllegalStateException("No LLM models found. Please ensure that you have the required dependencies in your classpath."); + throw new IllegalStateException( + "No LLM models found. Please ensure that you have the required dependencies in your classpath."); } private ChatModelService getAvailableModel() { @@ -124,7 +136,8 @@ private ChatModelService getAvailableModel() { return service; } - throw new IllegalStateException("No LLM models found. Please ensure that you have the required dependencies in your classpath."); + throw new IllegalStateException( + "No LLM models found. Please ensure that you have the required dependencies in your classpath."); } private Map prepareResponse(ChatResponse response, Object javaObject) { diff --git a/experimental/ai/pom.xml b/experimental/ai/pom.xml index 9d774798..2999c3a7 100644 --- a/experimental/ai/pom.xml +++ b/experimental/ai/pom.xml @@ -13,5 +13,7 @@ pom impl + types + models \ No newline at end of file diff --git a/experimental/ai/types/pom.xml b/experimental/ai/types/pom.xml new file mode 100644 index 00000000..6e0d483e --- /dev/null +++ b/experimental/ai/types/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-experimental-ai-parent + 8.0.0-SNAPSHOT + + serverlessworkflow-experimental-ai-types + ServelessWorkflow:: Experimental:: AI:: Types + + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + dev.langchain4j + langchain4j + 1.1.0 + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + ch.qos.logback + logback-classic + test + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + + \ No newline at end of file diff --git a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java new file mode 100644 index 00000000..f1ec2355 --- /dev/null +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.ai.api.types; + +import dev.langchain4j.model.chat.ChatModel; +import io.serverlessworkflow.api.types.TaskBase; + +public class CallAILangChainChatModel extends TaskBase { + + private final ChatModel chatModel; + private final Class chatModelRequest; + + public CallAILangChainChatModel(ChatModel chatModel, Class chatModelRequest) { + this.chatModel = chatModel; + this.chatModelRequest = chatModelRequest; + } + + public ChatModel getChatModel() { + return chatModel; + } + + public Class getChatModelRequest() { + return chatModelRequest; + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java index 61d0e619..7da97058 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java @@ -16,20 +16,19 @@ package io.serverlessworkflow.api.types.ai; +import io.serverlessworkflow.api.types.TaskBase; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import io.serverlessworkflow.api.types.TaskBase; - public class CallAIChatModel extends TaskBase { private ChatModelPreferences chatModelPreferences; private ChatModelRequest chatModelRequest; - private CallAIChatModel() {} + protected CallAIChatModel() {} public static Builder builder() { return new Builder(); From c6587b6ad93c177c9146859ed18a4d8b4900f12b Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 22 Jul 2025 22:15:34 -0700 Subject: [PATCH 3/6] LangChainChatModel done --- experimental/ai/impl/pom.xml | 20 ++- .../executors/ai/AIChatModelCallExecutor.java | 138 +----------------- .../ai/AbstractCallAIChatModelExecutor.java | 61 ++++++++ .../executors/ai/CallAIChatModelExecutor.java | 108 ++++++++++++++ .../ai/CallAILangChainChatModelExecutor.java | 95 ++++++++++++ .../api/types/CallAILangChainChatModel.java | 14 +- experimental/pom.xml | 7 +- .../types/ai/AbstractCallAIChatModelTask.java | 21 +++ .../api/types/ai/CallAIChatModel.java | 3 +- .../api/types/ai/CallTaskAIChatModel.java | 6 +- 10 files changed, 321 insertions(+), 152 deletions(-) create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java create mode 100644 experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java diff --git a/experimental/ai/impl/pom.xml b/experimental/ai/impl/pom.xml index 2f7c9521..91251f79 100644 --- a/experimental/ai/impl/pom.xml +++ b/experimental/ai/impl/pom.xml @@ -19,6 +19,14 @@ io.serverlessworkflow serverlessworkflow-impl-core + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + io.serverlessworkflow + serverlessworkflow-experimental-ai-types + dev.langchain4j langchain4j @@ -50,15 +58,5 @@ logback-classic test - - io.serverlessworkflow - serverlessworkflow-experimental-types - - - io.serverlessworkflow - serverlessworkflow-experimental-ai-types - 8.0.0-SNAPSHOT - compile - - \ No newline at end of file + diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java index 5b3f48df..a7266e82 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java @@ -16,14 +16,6 @@ package io.serverlessworkflow.impl.executors.ai; -import dev.langchain4j.data.message.AiMessage; -import dev.langchain4j.data.message.ChatMessage; -import dev.langchain4j.data.message.SystemMessage; -import dev.langchain4j.data.message.UserMessage; -import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.chat.response.ChatResponse; -import dev.langchain4j.model.output.FinishReason; -import dev.langchain4j.model.output.TokenUsage; import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.ai.CallAIChatModel; @@ -34,21 +26,10 @@ import io.serverlessworkflow.impl.WorkflowModelFactory; import io.serverlessworkflow.impl.executors.CallableTask; import io.serverlessworkflow.impl.resources.ResourceLoader; -import io.serverlessworkflow.impl.services.ChatModelService; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class AIChatModelCallExecutor implements CallableTask { - private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*(.+?)\\s*\\}\\}"); - @Override public void init(CallAIChatModel task, WorkflowApplication application, ResourceLoader loader) {} @@ -58,12 +39,13 @@ public CompletableFuture apply( WorkflowModelFactory modelFactory = workflowContext.definition().application().modelFactory(); if (taskContext.task() instanceof CallAILangChainChatModel callAILangChainChatModel) { return CompletableFuture.completedFuture( - modelFactory.fromAny(doCall(callAILangChainChatModel, input.asJavaObject()))); - } - - if (taskContext.task() instanceof CallAIChatModel callAIChatModel) { + modelFactory.fromAny( + new CallAILangChainChatModelExecutor() + .apply(callAILangChainChatModel, input.asJavaObject()))); + } else if (taskContext.task() instanceof CallAIChatModel callAIChatModel) { return CompletableFuture.completedFuture( - modelFactory.fromAny(doCall(callAIChatModel, input.asJavaObject()))); + modelFactory.fromAny( + new CallAIChatModelExecutor().apply(callAIChatModel, input.asJavaObject()))); } throw new IllegalArgumentException( "AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " @@ -74,112 +56,4 @@ public CompletableFuture apply( public boolean accept(Class clazz) { return CallAIChatModel.class.isAssignableFrom(clazz); } - - private Object doCall(CallAILangChainChatModel callAIChatModel, Object javaObject) { - ChatModel chatModel = callAIChatModel.getChatModel(); - Class chatModelRequest = callAIChatModel.getChatModelRequest(); - } - - private Object doCall(CallAIChatModel callAIChatModel, Object javaObject) { - validate(callAIChatModel, javaObject); - ChatModel chatModel = createChatModel(callAIChatModel); - Map substitutions = (Map) javaObject; - - List messages = new ArrayList<>(); - - if (callAIChatModel.getChatModelRequest().getSystemMessages() != null) { - for (String systemMessage : callAIChatModel.getChatModelRequest().getSystemMessages()) { - String fixedUserMessage = replaceVariables(systemMessage, substitutions); - messages.add(new SystemMessage(fixedUserMessage)); - } - } - - if (callAIChatModel.getChatModelRequest().getUserMessages() != null) { - for (String userMessage : callAIChatModel.getChatModelRequest().getUserMessages()) { - String fixedUserMessage = replaceVariables(userMessage, substitutions); - messages.add(new UserMessage(fixedUserMessage)); - } - } - - return prepareResponse(chatModel.chat(messages), javaObject); - } - - private String replaceVariables(String template, Map substitutions) { - Set variables = extractVariables(template); - for (Map.Entry entry : substitutions.entrySet()) { - String variable = entry.getKey(); - Object value = entry.getValue(); - if (value != null && variables.contains(variable)) { - template = template.replace("{{" + variable + "}}", value.toString()); - } - } - return template; - } - - private void validate(CallAIChatModel callAIChatModel, Object javaObject) { - // TODO - } - - private ChatModel createChatModel(CallAIChatModel callAIChatModel) { - ChatModelService chatModelService = getAvailableModel(); - if (chatModelService != null) { - return chatModelService.getChatModel(callAIChatModel.getPreferences()); - } - throw new IllegalStateException( - "No LLM models found. Please ensure that you have the required dependencies in your classpath."); - } - - private ChatModelService getAvailableModel() { - ServiceLoader loader = ServiceLoader.load(ChatModelService.class); - - for (ChatModelService service : loader) { - return service; - } - - throw new IllegalStateException( - "No LLM models found. Please ensure that you have the required dependencies in your classpath."); - } - - private Map prepareResponse(ChatResponse response, Object javaObject) { - - String id = response.id(); - String modelName = response.modelName(); - TokenUsage tokenUsage = response.tokenUsage(); - FinishReason finishReason = response.finishReason(); - AiMessage aiMessage = response.aiMessage(); - - Map responseMap = (Map) javaObject; - if (response.id() != null) { - responseMap.put("id", id); - } - - if (modelName != null) { - responseMap.put("modelName", modelName); - } - - if (tokenUsage != null) { - responseMap.put("tokenUsage.inputTokenCount", tokenUsage.inputTokenCount()); - responseMap.put("tokenUsage.outputTokenCount", tokenUsage.outputTokenCount()); - responseMap.put("tokenUsage.totalTokenCount", tokenUsage.totalTokenCount()); - } - - if (finishReason != null) { - responseMap.put("finishReason", finishReason.name()); - } - - if (aiMessage != null) { - responseMap.put("text", aiMessage.text()); - } - - return responseMap; - } - - private static Set extractVariables(String template) { - Set variables = new HashSet<>(); - Matcher matcher = VARIABLE_PATTERN.matcher(template); - while (matcher.find()) { - variables.add(matcher.group(1)); - } - return variables; - } } diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java new file mode 100644 index 00000000..23f48862 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.executors.ai; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.model.output.TokenUsage; +import java.util.Map; + +public abstract class AbstractCallAIChatModelExecutor { + + public abstract Object apply(T callAIChatModel, Object javaObject); + + protected Map prepareResponse(ChatResponse response, Object javaObject) { + String id = response.id(); + String modelName = response.modelName(); + TokenUsage tokenUsage = response.tokenUsage(); + FinishReason finishReason = response.finishReason(); + AiMessage aiMessage = response.aiMessage(); + + Map responseMap = (Map) javaObject; + if (response.id() != null) { + responseMap.put("id", id); + } + + if (modelName != null) { + responseMap.put("modelName", modelName); + } + + if (tokenUsage != null) { + responseMap.put("tokenUsage.inputTokenCount", tokenUsage.inputTokenCount()); + responseMap.put("tokenUsage.outputTokenCount", tokenUsage.outputTokenCount()); + responseMap.put("tokenUsage.totalTokenCount", tokenUsage.totalTokenCount()); + } + + if (finishReason != null) { + responseMap.put("finishReason", finishReason.name()); + } + + if (aiMessage != null) { + responseMap.put("text", aiMessage.text()); + } + + return responseMap; + } +} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java new file mode 100644 index 00000000..dd4217b4 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.executors.ai; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatModel; +import io.serverlessworkflow.api.types.ai.CallAIChatModel; +import io.serverlessworkflow.impl.services.ChatModelService; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CallAIChatModelExecutor extends AbstractCallAIChatModelExecutor { + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*(.+?)\\s*\\}\\}"); + + @Override + public Object apply(CallAIChatModel callAIChatModel, Object javaObject) { + validate(callAIChatModel, javaObject); + + ChatModel chatModel = createChatModel(callAIChatModel); + Map substitutions = (Map) javaObject; + + List messages = new ArrayList<>(); + + if (callAIChatModel.getChatModelRequest().getSystemMessages() != null) { + for (String systemMessage : callAIChatModel.getChatModelRequest().getSystemMessages()) { + String fixedUserMessage = replaceVariables(systemMessage, substitutions); + messages.add(new SystemMessage(fixedUserMessage)); + } + } + + if (callAIChatModel.getChatModelRequest().getUserMessages() != null) { + for (String userMessage : callAIChatModel.getChatModelRequest().getUserMessages()) { + String fixedUserMessage = replaceVariables(userMessage, substitutions); + messages.add(new UserMessage(fixedUserMessage)); + } + } + + return prepareResponse(chatModel.chat(messages), javaObject); + } + + private ChatModel createChatModel(CallAIChatModel callAIChatModel) { + ChatModelService chatModelService = getAvailableModel(); + if (chatModelService != null) { + return chatModelService.getChatModel(callAIChatModel.getPreferences()); + } + throw new IllegalStateException( + "No LLM models found. Please ensure that you have the required dependencies in your classpath."); + } + + private String replaceVariables(String template, Map substitutions) { + Set variables = extractVariables(template); + for (Map.Entry entry : substitutions.entrySet()) { + String variable = entry.getKey(); + Object value = entry.getValue(); + if (value != null && variables.contains(variable)) { + template = template.replace("{{" + variable + "}}", value.toString()); + } + } + return template; + } + + private ChatModelService getAvailableModel() { + ServiceLoader loader = ServiceLoader.load(ChatModelService.class); + + for (ChatModelService service : loader) { + return service; + } + + throw new IllegalStateException( + "No LLM models found. Please ensure that you have the required dependencies in your classpath."); + } + + private static Set extractVariables(String template) { + Set variables = new HashSet<>(); + Matcher matcher = VARIABLE_PATTERN.matcher(template); + while (matcher.find()) { + variables.add(matcher.group(1)); + } + return variables; + } + + private void validate(CallAIChatModel callAIChatModel, Object javaObject) { + // TODO + } +} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java new file mode 100644 index 00000000..998d9fbb --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.impl.executors.ai; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.V; +import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CallAILangChainChatModelExecutor + extends AbstractCallAIChatModelExecutor { + + @Override + public Object apply(CallAILangChainChatModel callAIChatModel, Object javaObject) { + ChatModel chatModel = callAIChatModel.getChatModel(); + Class chatModelRequest = callAIChatModel.getChatModelRequest(); + Map substitutions = (Map) javaObject; + validate(chatModel, chatModelRequest, substitutions); + + Method method = getMethod(chatModelRequest, callAIChatModel.getMethodName()); + List resolvedParameters = resolvedParameters(method, substitutions); + + var aiServices = AiServices.builder(chatModelRequest).chatModel(chatModel).build(); + try { + Object[] args = new Object[resolvedParameters.size()]; + for (int i = 0; i < resolvedParameters.size(); i++) { + String paramName = resolvedParameters.get(i); + args[i] = substitutions.get(paramName); + } + + Object response = method.invoke(aiServices, args); + if (response instanceof ChatResponse chatResponse) { + return prepareResponse(chatResponse, substitutions); + } else { + throw new IllegalArgumentException( + "Method " + method.getName() + " did not return a ChatResponse"); + } + } catch (Exception e) { + throw new RuntimeException("Error invoking chat model method", e); + } + } + + private void validate( + ChatModel chatModel, Class chatModelRequest, Map substitutions) {} + + private Method getMethod(Class chatModelRequest, String methodName) { + for (Method method : chatModelRequest.getMethods()) { + if (method.getName().equals("methodName")) { + return method; + } + } + throw new IllegalArgumentException( + "Method " + methodName + " not found in class " + chatModelRequest.getName()); + } + + private List resolvedParameters(Method method, Map substitutions) { + List resolvedParameters = new ArrayList<>(); + for (Parameter parameter : method.getParameters()) { + String paramName = resolveParameter(parameter); + if (substitutions.containsKey(paramName)) { + resolvedParameters.add(paramName); + } else { + throw new IllegalArgumentException("Missing substitution for parameter: " + paramName); + } + } + return resolvedParameters; + } + + private String resolveParameter(Parameter parameter) { + if (parameter.getAnnotation(V.class) != null) { + return parameter.getName(); + } + return parameter.getName(); + } +} diff --git a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java index f1ec2355..74b87bd5 100644 --- a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java @@ -17,16 +17,20 @@ package io.serverlessworkflow.ai.api.types; import dev.langchain4j.model.chat.ChatModel; -import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.ai.AbstractCallAIChatModelTask; -public class CallAILangChainChatModel extends TaskBase { +public class CallAILangChainChatModel extends AbstractCallAIChatModelTask { private final ChatModel chatModel; private final Class chatModelRequest; - public CallAILangChainChatModel(ChatModel chatModel, Class chatModelRequest) { + private final String methodName; + + public CallAILangChainChatModel( + ChatModel chatModel, Class chatModelRequest, String methodName) { this.chatModel = chatModel; this.chatModelRequest = chatModelRequest; + this.methodName = methodName; } public ChatModel getChatModel() { @@ -36,4 +40,8 @@ public ChatModel getChatModel() { public Class getChatModelRequest() { return chatModelRequest; } + + public String getMethodName() { + return methodName; + } } diff --git a/experimental/pom.xml b/experimental/pom.xml index 84964c61..1c5efd8d 100644 --- a/experimental/pom.xml +++ b/experimental/pom.xml @@ -35,6 +35,11 @@ serverlessworkflow-fluent-func ${project.version} + + io.serverlessworkflow + serverlessworkflow-experimental-ai-types + ${project.version} + @@ -42,4 +47,4 @@ lambda ai - \ No newline at end of file + diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java new file mode 100644 index 00000000..63bd40fb --- /dev/null +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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 + * + * http://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 io.serverlessworkflow.api.types.ai; + +import io.serverlessworkflow.api.types.TaskBase; + +public abstract class AbstractCallAIChatModelTask extends TaskBase {} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java index 7da97058..0c10371a 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java @@ -16,13 +16,12 @@ package io.serverlessworkflow.api.types.ai; -import io.serverlessworkflow.api.types.TaskBase; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; -public class CallAIChatModel extends TaskBase { +public class CallAIChatModel extends AbstractCallAIChatModelTask { private ChatModelPreferences chatModelPreferences; diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java index e64fff12..4e2bd241 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java @@ -20,13 +20,13 @@ public class CallTaskAIChatModel extends CallTask { - private CallAIChatModel callAIChatModel; + private AbstractCallAIChatModelTask callAIChatModel; - public CallTaskAIChatModel(CallAIChatModel callAIChatModel) { + public CallTaskAIChatModel(AbstractCallAIChatModelTask callAIChatModel) { this.callAIChatModel = callAIChatModel; } - public CallAIChatModel getCallAIChatModel() { + public AbstractCallAIChatModelTask getCallAIChatModel() { return callAIChatModel; } From a8a4b600c9486b482da245aa290a84284974f6a4 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 23 Jul 2025 10:54:00 -0700 Subject: [PATCH 4/6] CallAILangChainChatModel works --- .../executors/ai/AIChatModelCallExecutor.java | 8 ++-- .../ai/AbstractCallAIChatModelExecutor.java | 39 ------------------- .../executors/ai/CallAIChatModelExecutor.java | 37 ++++++++++++++++++ .../ai/CallAILangChainChatModelExecutor.java | 29 +++++++------- 4 files changed, 55 insertions(+), 58 deletions(-) diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java index a7266e82..0e261fad 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java @@ -18,6 +18,7 @@ import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.ai.AbstractCallAIChatModelTask; import io.serverlessworkflow.api.types.ai.CallAIChatModel; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; @@ -28,10 +29,11 @@ import io.serverlessworkflow.impl.resources.ResourceLoader; import java.util.concurrent.CompletableFuture; -public class AIChatModelCallExecutor implements CallableTask { +public class AIChatModelCallExecutor implements CallableTask { @Override - public void init(CallAIChatModel task, WorkflowApplication application, ResourceLoader loader) {} + public void init( + AbstractCallAIChatModelTask task, WorkflowApplication application, ResourceLoader loader) {} @Override public CompletableFuture apply( @@ -54,6 +56,6 @@ public CompletableFuture apply( @Override public boolean accept(Class clazz) { - return CallAIChatModel.class.isAssignableFrom(clazz); + return AbstractCallAIChatModelTask.class.isAssignableFrom(clazz); } } diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java index 23f48862..1b94ebdf 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java @@ -16,46 +16,7 @@ package io.serverlessworkflow.impl.executors.ai; -import dev.langchain4j.data.message.AiMessage; -import dev.langchain4j.model.chat.response.ChatResponse; -import dev.langchain4j.model.output.FinishReason; -import dev.langchain4j.model.output.TokenUsage; -import java.util.Map; - public abstract class AbstractCallAIChatModelExecutor { public abstract Object apply(T callAIChatModel, Object javaObject); - - protected Map prepareResponse(ChatResponse response, Object javaObject) { - String id = response.id(); - String modelName = response.modelName(); - TokenUsage tokenUsage = response.tokenUsage(); - FinishReason finishReason = response.finishReason(); - AiMessage aiMessage = response.aiMessage(); - - Map responseMap = (Map) javaObject; - if (response.id() != null) { - responseMap.put("id", id); - } - - if (modelName != null) { - responseMap.put("modelName", modelName); - } - - if (tokenUsage != null) { - responseMap.put("tokenUsage.inputTokenCount", tokenUsage.inputTokenCount()); - responseMap.put("tokenUsage.outputTokenCount", tokenUsage.outputTokenCount()); - responseMap.put("tokenUsage.totalTokenCount", tokenUsage.totalTokenCount()); - } - - if (finishReason != null) { - responseMap.put("finishReason", finishReason.name()); - } - - if (aiMessage != null) { - responseMap.put("text", aiMessage.text()); - } - - return responseMap; - } } diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java index dd4217b4..cce3db2a 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java @@ -16,10 +16,14 @@ package io.serverlessworkflow.impl.executors.ai; +import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.model.output.TokenUsage; import io.serverlessworkflow.api.types.ai.CallAIChatModel; import io.serverlessworkflow.impl.services.ChatModelService; import java.util.ArrayList; @@ -105,4 +109,37 @@ private static Set extractVariables(String template) { private void validate(CallAIChatModel callAIChatModel, Object javaObject) { // TODO } + + protected Map prepareResponse(ChatResponse response, Object javaObject) { + String id = response.id(); + String modelName = response.modelName(); + TokenUsage tokenUsage = response.tokenUsage(); + FinishReason finishReason = response.finishReason(); + AiMessage aiMessage = response.aiMessage(); + + Map responseMap = (Map) javaObject; + if (response.id() != null) { + responseMap.put("id", id); + } + + if (modelName != null) { + responseMap.put("modelName", modelName); + } + + if (tokenUsage != null) { + responseMap.put("tokenUsage.inputTokenCount", tokenUsage.inputTokenCount()); + responseMap.put("tokenUsage.outputTokenCount", tokenUsage.outputTokenCount()); + responseMap.put("tokenUsage.totalTokenCount", tokenUsage.totalTokenCount()); + } + + if (finishReason != null) { + responseMap.put("finishReason", finishReason.name()); + } + + if (aiMessage != null) { + responseMap.put("text", aiMessage.text()); + } + + return responseMap; + } } diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java index 998d9fbb..3e8ab1ec 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java @@ -17,7 +17,6 @@ package io.serverlessworkflow.impl.executors.ai; import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.V; import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; @@ -49,8 +48,10 @@ public Object apply(CallAILangChainChatModel callAIChatModel, Object javaObject) } Object response = method.invoke(aiServices, args); - if (response instanceof ChatResponse chatResponse) { - return prepareResponse(chatResponse, substitutions); + + if (response instanceof String chatResponse) { + substitutions.put("text", chatResponse); + return substitutions; } else { throw new IllegalArgumentException( "Method " + method.getName() + " did not return a ChatResponse"); @@ -65,7 +66,7 @@ private void validate( private Method getMethod(Class chatModelRequest, String methodName) { for (Method method : chatModelRequest.getMethods()) { - if (method.getName().equals("methodName")) { + if (method.getName().equals(methodName)) { return method; } } @@ -76,20 +77,16 @@ private Method getMethod(Class chatModelRequest, String methodName) { private List resolvedParameters(Method method, Map substitutions) { List resolvedParameters = new ArrayList<>(); for (Parameter parameter : method.getParameters()) { - String paramName = resolveParameter(parameter); - if (substitutions.containsKey(paramName)) { - resolvedParameters.add(paramName); - } else { - throw new IllegalArgumentException("Missing substitution for parameter: " + paramName); + if (parameter.getAnnotation(V.class) != null) { + V v = parameter.getAnnotation(V.class); + String paramName = v.value(); + if (substitutions.containsKey(paramName)) { + resolvedParameters.add(paramName); + } else { + throw new IllegalArgumentException("Missing substitution for parameter: " + paramName); + } } } return resolvedParameters; } - - private String resolveParameter(Parameter parameter) { - if (parameter.getAnnotation(V.class) != null) { - return parameter.getName(); - } - return parameter.getName(); - } } From b3da32d0d68bbb5d97eecdef1615c1b36fd292f6 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 23 Jul 2025 16:15:43 -0700 Subject: [PATCH 5/6] text -> result --- .../impl/executors/ai/CallAIChatModelExecutor.java | 2 +- .../executors/ai/CallAILangChainChatModelExecutor.java | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java index cce3db2a..86635499 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java @@ -137,7 +137,7 @@ protected Map prepareResponse(ChatResponse response, Object java } if (aiMessage != null) { - responseMap.put("text", aiMessage.text()); + responseMap.put("result", aiMessage.text()); } return responseMap; diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java index 3e8ab1ec..b7238402 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java @@ -48,17 +48,11 @@ public Object apply(CallAILangChainChatModel callAIChatModel, Object javaObject) } Object response = method.invoke(aiServices, args); - - if (response instanceof String chatResponse) { - substitutions.put("text", chatResponse); - return substitutions; - } else { - throw new IllegalArgumentException( - "Method " + method.getName() + " did not return a ChatResponse"); - } + substitutions.put("result", response); } catch (Exception e) { throw new RuntimeException("Error invoking chat model method", e); } + return substitutions; } private void validate( From 7c4acac366799af5ab06d856c1acc272160d3e88 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Thu, 24 Jul 2025 12:18:30 +0200 Subject: [PATCH 6/6] Review comments --- experimental/ai/impl/pom.xml | 9 ------- .../executors/ai/AIChatModelCallExecutor.java | 27 +++++++++---------- ...Executor.java => AIChatModelExecutor.java} | 5 ++-- .../executors/ai/CallAIChatModelExecutor.java | 10 +++++-- .../ai/CallAILangChainChatModelExecutor.java | 13 ++++++--- .../impl/services/ChatModelService.java | 1 - ...orkflow.impl.executors.TaskExecutorFactory | 1 - experimental/ai/types/pom.xml | 5 ---- .../types/ai/AbstractCallAIChatModelTask.java | 0 .../api/types/ai/CallAIChatModel.java | 0 .../types/ai}/CallAILangChainChatModel.java | 3 +-- .../api/types/ai/CallTaskAIChatModel.java | 0 12 files changed, 32 insertions(+), 42 deletions(-) rename experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/{AbstractCallAIChatModelExecutor.java => AIChatModelExecutor.java} (83%) delete mode 100644 experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory rename experimental/{ => ai}/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java (100%) rename experimental/{ => ai}/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java (100%) rename experimental/ai/types/src/main/java/io/serverlessworkflow/{ai/api/types => api/types/ai}/CallAILangChainChatModel.java (91%) rename experimental/{ => ai}/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java (100%) diff --git a/experimental/ai/impl/pom.xml b/experimental/ai/impl/pom.xml index 91251f79..4757feca 100644 --- a/experimental/ai/impl/pom.xml +++ b/experimental/ai/impl/pom.xml @@ -11,18 +11,10 @@ serverlessworkflow-experimental-ai-impl ServelessWorkflow:: Experimental:: AI:: Impl - - io.serverlessworkflow - serverlessworkflow-experimental-types - io.serverlessworkflow serverlessworkflow-impl-core - - io.serverlessworkflow - serverlessworkflow-experimental-types - io.serverlessworkflow serverlessworkflow-experimental-ai-types @@ -31,7 +23,6 @@ dev.langchain4j langchain4j 1.1.0 - true org.junit.jupiter diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java index 0e261fad..adfbba91 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelCallExecutor.java @@ -16,10 +16,10 @@ package io.serverlessworkflow.impl.executors.ai; -import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.ai.AbstractCallAIChatModelTask; import io.serverlessworkflow.api.types.ai.CallAIChatModel; +import io.serverlessworkflow.api.types.ai.CallAILangChainChatModel; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContext; @@ -31,27 +31,24 @@ public class AIChatModelCallExecutor implements CallableTask { + private AIChatModelExecutor executor; + @Override public void init( - AbstractCallAIChatModelTask task, WorkflowApplication application, ResourceLoader loader) {} + AbstractCallAIChatModelTask task, WorkflowApplication application, ResourceLoader loader) { + if (task instanceof CallAILangChainChatModel model) { + executor = new CallAILangChainChatModelExecutor(model); + } else if (task instanceof CallAIChatModel model) { + executor = new CallAIChatModelExecutor(model); + } + } @Override public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { WorkflowModelFactory modelFactory = workflowContext.definition().application().modelFactory(); - if (taskContext.task() instanceof CallAILangChainChatModel callAILangChainChatModel) { - return CompletableFuture.completedFuture( - modelFactory.fromAny( - new CallAILangChainChatModelExecutor() - .apply(callAILangChainChatModel, input.asJavaObject()))); - } else if (taskContext.task() instanceof CallAIChatModel callAIChatModel) { - return CompletableFuture.completedFuture( - modelFactory.fromAny( - new CallAIChatModelExecutor().apply(callAIChatModel, input.asJavaObject()))); - } - throw new IllegalArgumentException( - "AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " - + taskContext.task().getClass().getName()); + return CompletableFuture.completedFuture( + modelFactory.fromAny(executor.apply(input.asJavaObject()))); } @Override diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelExecutor.java similarity index 83% rename from experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java rename to experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelExecutor.java index 1b94ebdf..4d40a591 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AIChatModelExecutor.java @@ -16,7 +16,6 @@ package io.serverlessworkflow.impl.executors.ai; -public abstract class AbstractCallAIChatModelExecutor { +import java.util.function.UnaryOperator; - public abstract Object apply(T callAIChatModel, Object javaObject); -} +public interface AIChatModelExecutor extends UnaryOperator {} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java index 86635499..0eb51c0c 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java @@ -35,12 +35,18 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class CallAIChatModelExecutor extends AbstractCallAIChatModelExecutor { +public class CallAIChatModelExecutor implements AIChatModelExecutor { private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{\\s*(.+?)\\s*\\}\\}"); + private final CallAIChatModel callAIChatModel; + + public CallAIChatModelExecutor(CallAIChatModel callAIChatModel) { + this.callAIChatModel = callAIChatModel; + } + @Override - public Object apply(CallAIChatModel callAIChatModel, Object javaObject) { + public Object apply(Object javaObject) { validate(callAIChatModel, javaObject); ChatModel chatModel = createChatModel(callAIChatModel); diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java index b7238402..9ed9e760 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java @@ -19,18 +19,23 @@ import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.V; -import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; +import io.serverlessworkflow.api.types.ai.CallAILangChainChatModel; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.List; import java.util.Map; -public class CallAILangChainChatModelExecutor - extends AbstractCallAIChatModelExecutor { +public class CallAILangChainChatModelExecutor implements AIChatModelExecutor { + + private final CallAILangChainChatModel callAIChatModel; + + public CallAILangChainChatModelExecutor(CallAILangChainChatModel callAIChatModel) { + this.callAIChatModel = callAIChatModel; + } @Override - public Object apply(CallAILangChainChatModel callAIChatModel, Object javaObject) { + public Object apply(Object javaObject) { ChatModel chatModel = callAIChatModel.getChatModel(); Class chatModelRequest = callAIChatModel.getChatModelRequest(); Map substitutions = (Map) javaObject; diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java index ddbc4be2..5c4d8214 100644 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java @@ -20,6 +20,5 @@ import io.serverlessworkflow.api.types.ai.CallAIChatModel; public interface ChatModelService { - ChatModel getChatModel(CallAIChatModel.ChatModelPreferences chatModelPreferences); } diff --git a/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory b/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory deleted file mode 100644 index a370284d..00000000 --- a/experimental/ai/impl/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.TaskExecutorFactory +++ /dev/null @@ -1 +0,0 @@ -io.serverlessworkflow.impl.executors.ai.AIChatModelTaskExecutorFactory \ No newline at end of file diff --git a/experimental/ai/types/pom.xml b/experimental/ai/types/pom.xml index 6e0d483e..31f04713 100644 --- a/experimental/ai/types/pom.xml +++ b/experimental/ai/types/pom.xml @@ -16,15 +16,10 @@ io.serverlessworkflow serverlessworkflow-experimental-types - - io.serverlessworkflow - serverlessworkflow-impl-core - dev.langchain4j langchain4j 1.1.0 - true org.junit.jupiter diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java similarity index 100% rename from experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java rename to experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java similarity index 100% rename from experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java rename to experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java diff --git a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAILangChainChatModel.java similarity index 91% rename from experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java rename to experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAILangChainChatModel.java index 74b87bd5..da24b5c8 100644 --- a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAILangChainChatModel.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package io.serverlessworkflow.ai.api.types; +package io.serverlessworkflow.api.types.ai; import dev.langchain4j.model.chat.ChatModel; -import io.serverlessworkflow.api.types.ai.AbstractCallAIChatModelTask; public class CallAILangChainChatModel extends AbstractCallAIChatModelTask { diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java similarity index 100% rename from experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java rename to experimental/ai/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java