From 4a0a2a775bfed8c2afd53455cb0a2692eebd89ed Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 22 Jul 2025 14:41:18 -0700 Subject: [PATCH 1/7] 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/7] 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/7] 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/7] 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/7] 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 df44f19bb666032fa2ce1fbd8d83bafe3dd403a4 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 24 Jul 2025 17:02:29 -0700 Subject: [PATCH 6/7] use langchain4g agentic api for agents --- experimental/ai/impl/pom.xml | 5 - .../executors/ai/AIChatModelCallExecutor.java | 23 +- .../ai/AbstractCallAIChatModelExecutor.java | 22 - .../executors/ai/CallAIChatModelExecutor.java | 145 ------ .../ai/CallAILangChainChatModelExecutor.java | 86 ---- .../executors/ai/CallAgentAIExecutor.java | 41 ++ .../impl/services/ChatModelService.java | 25 - 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 | 1 - experimental/ai/types/pom.xml | 11 +- .../api/types/CallAILangChainChatModel.java | 47 -- .../ai/api/types/CallAgentAI.java | 86 ++++ .../ai/api/types}/CallTaskAIChatModel.java | 8 +- .../types/ai/AbstractCallAIChatModelTask.java | 21 - .../api/types/ai/CallAIChatModel.java | 438 ------------------ 18 files changed, 146 insertions(+), 956 deletions(-) delete mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java delete mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java delete mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java create mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAgentAIExecutor.java delete mode 100644 experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java delete mode 100644 experimental/ai/models/openai/pom.xml delete mode 100644 experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java delete mode 100644 experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService delete mode 100644 experimental/ai/models/pom.xml delete mode 100644 experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java create mode 100644 experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java rename experimental/{types/src/main/java/io/serverlessworkflow/api/types/ai => ai/types/src/main/java/io/serverlessworkflow/ai/api/types}/CallTaskAIChatModel.java (79%) delete mode 100644 experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java delete mode 100644 experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java diff --git a/experimental/ai/impl/pom.xml b/experimental/ai/impl/pom.xml index 91251f79..566efcc5 100644 --- a/experimental/ai/impl/pom.xml +++ b/experimental/ai/impl/pom.xml @@ -19,10 +19,6 @@ io.serverlessworkflow serverlessworkflow-impl-core - - io.serverlessworkflow - serverlessworkflow-experimental-types - io.serverlessworkflow serverlessworkflow-experimental-ai-types @@ -31,7 +27,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..7d64bb9a 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,8 @@ package io.serverlessworkflow.impl.executors.ai; -import io.serverlessworkflow.ai.api.types.CallAILangChainChatModel; +import io.serverlessworkflow.ai.api.types.CallAgentAI; 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; import io.serverlessworkflow.impl.WorkflowContext; @@ -29,33 +27,26 @@ 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( - AbstractCallAIChatModelTask task, WorkflowApplication application, ResourceLoader loader) {} + public void init(CallAgentAI 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 CallAILangChainChatModel callAILangChainChatModel) { + if (taskContext.task() instanceof CallAgentAI agenticAI) { 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()))); + modelFactory.fromAny(new CallAgentAIExecutor().execute(agenticAI, input.asJavaObject()))); } throw new IllegalArgumentException( - "AIChatModelCallExecutor can only process CallAIChatModel tasks, but received: " + "AIChatModelCallExecutor can only process CallAgentAI tasks, but received: " + taskContext.task().getClass().getName()); } @Override public boolean accept(Class clazz) { - return AbstractCallAIChatModelTask.class.isAssignableFrom(clazz); + return CallAgentAI.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 deleted file mode 100644 index 1b94ebdf..00000000 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/AbstractCallAIChatModelExecutor.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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; - -public abstract class AbstractCallAIChatModelExecutor { - - public abstract Object apply(T callAIChatModel, Object javaObject); -} 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 deleted file mode 100644 index 86635499..00000000 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAIChatModelExecutor.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.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; -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 - } - - 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("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 deleted file mode 100644 index b7238402..00000000 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAILangChainChatModelExecutor.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.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); - substitutions.put("result", response); - } catch (Exception e) { - throw new RuntimeException("Error invoking chat model method", e); - } - return substitutions; - } - - 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()) { - 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; - } -} diff --git a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAgentAIExecutor.java b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAgentAIExecutor.java new file mode 100644 index 00000000..b13ab474 --- /dev/null +++ b/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/executors/ai/CallAgentAIExecutor.java @@ -0,0 +1,41 @@ +/* + * 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.agentic.Cognisphere; +import dev.langchain4j.agentic.internal.AgentExecutor; +import dev.langchain4j.agentic.internal.AgentUtil; +import io.serverlessworkflow.ai.api.types.CallAgentAI; +import java.util.Map; + +public class CallAgentAIExecutor { + + private static final Cognisphere cognisphere = new Cognisphere(); + + public Object execute(CallAgentAI callAgentAI, Object input) { + AgentExecutor agentExecutor = AgentUtil.agentToExecutor(callAgentAI.getAgentInstance()); + + Map output = (Map) input; + + cognisphere.writeStates(output); + + Object result = agentExecutor.invoke(cognisphere); + + output.put(callAgentAI.getAgentInstance().outputName(), result); + return output; + } +} 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 deleted file mode 100644 index ddbc4be2..00000000 --- a/experimental/ai/impl/src/main/java/io/serverlessworkflow/impl/services/ChatModelService.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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/models/openai/pom.xml b/experimental/ai/models/openai/pom.xml deleted file mode 100644 index 42135bfd..00000000 --- a/experimental/ai/models/openai/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - 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 deleted file mode 100644 index b05eef89..00000000 --- a/experimental/ai/models/openai/src/main/java/io/serverlessworkflow/impl/services/openai/OpenAIChatModelService.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 deleted file mode 100644 index 7f1d56b1..00000000 --- a/experimental/ai/models/openai/src/main/resources/META-INF/services/io.serverlessworkflow.impl.services.ChatModelService +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index b31fc70d..00000000 --- a/experimental/ai/models/pom.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - 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 index 2999c3a7..6856cf14 100644 --- a/experimental/ai/pom.xml +++ b/experimental/ai/pom.xml @@ -14,6 +14,5 @@ impl types - models \ No newline at end of file diff --git a/experimental/ai/types/pom.xml b/experimental/ai/types/pom.xml index 6e0d483e..3136b81d 100644 --- a/experimental/ai/types/pom.xml +++ b/experimental/ai/types/pom.xml @@ -22,9 +22,14 @@ dev.langchain4j - langchain4j - 1.1.0 - true + langchain4j-agentic + 1.2.0-beta8-SNAPSHOT + + + + dev.langchain4j + langchain4j-open-ai + 1.2.0-SNAPSHOT org.junit.jupiter 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 deleted file mode 100644 index 74b87bd5..00000000 --- a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAILangChainChatModel.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.ai.AbstractCallAIChatModelTask; - -public class CallAILangChainChatModel extends AbstractCallAIChatModelTask { - - private final ChatModel chatModel; - private final 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() { - return chatModel; - } - - public Class getChatModelRequest() { - return chatModelRequest; - } - - public String getMethodName() { - return methodName; - } -} diff --git a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java new file mode 100644 index 00000000..2f461371 --- /dev/null +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java @@ -0,0 +1,86 @@ +/* + * 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.agentic.AgentServices; +import dev.langchain4j.agentic.internal.AgentInstance; +import dev.langchain4j.model.chat.ChatModel; +import io.serverlessworkflow.api.types.TaskBase; +import java.util.Objects; + +public class CallAgentAI extends TaskBase { + + private final AgentInstance instance; + + public CallAgentAI(Object instance) { + if (!(instance instanceof AgentInstance)) { + throw new IllegalArgumentException("Instance must be subtype of AgentInstance"); + } + + this.instance = (AgentInstance) instance; + } + + public AgentInstance getAgentInstance() { + return instance; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Class agentClass; + + private ChatModel chatModel; + + private String outputName; + + private Builder() {} + + public CallAgentAI.Builder withAgentClass(Class agentClass) { + this.agentClass = agentClass; + return this; + } + + public CallAgentAI.Builder withChatModel(ChatModel chatModel) { + this.chatModel = chatModel; + return this; + } + + public CallAgentAI.Builder withOutputName(String outputName) { + this.outputName = outputName; + return this; + } + + public CallAgentAI build() { + Objects.requireNonNull(agentClass, "agentClass must be provided"); + Objects.requireNonNull(chatModel, "chatModel must be provided"); + Objects.requireNonNull(outputName, "outputName must be provided"); + + if (outputName.isBlank()) { + throw new IllegalArgumentException("outputName must not be blank"); + } + + return new CallAgentAI( + AgentServices.agentBuilder(agentClass) + .chatModel(chatModel) + .outputName(outputName) + .build()); + } + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallTaskAIChatModel.java similarity index 79% rename from experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java rename to experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallTaskAIChatModel.java index 4e2bd241..5dfec536 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallTaskAIChatModel.java +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallTaskAIChatModel.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package io.serverlessworkflow.api.types.ai; +package io.serverlessworkflow.ai.api.types; import io.serverlessworkflow.api.types.CallTask; public class CallTaskAIChatModel extends CallTask { - private AbstractCallAIChatModelTask callAIChatModel; + private CallAgentAI callAIChatModel; - public CallTaskAIChatModel(AbstractCallAIChatModelTask callAIChatModel) { + public CallTaskAIChatModel(CallAgentAI callAIChatModel) { this.callAIChatModel = callAIChatModel; } - public AbstractCallAIChatModelTask getCallAIChatModel() { + public CallAgentAI getCallAIChatModel() { return callAIChatModel; } 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 deleted file mode 100644 index 63bd40fb..00000000 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/AbstractCallAIChatModelTask.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 deleted file mode 100644 index 0c10371a..00000000 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/ai/CallAIChatModel.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * 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; - -public class CallAIChatModel extends AbstractCallAIChatModelTask { - - private ChatModelPreferences chatModelPreferences; - - private ChatModelRequest chatModelRequest; - - protected 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; - } - } - } -} From 4f19d52722f700b22e88bc7c05a37dbb41df1306 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 24 Jul 2025 23:15:37 -0700 Subject: [PATCH 7/7] add fluent api support for CallAgentAI --- .../ai/api/types/CallAgentAI.java | 39 ++++++++++++----- fluent/pom.xml | 7 ++- fluent/spec/pom.xml | 6 ++- .../fluent/spec/BaseDoTaskBuilder.java | 10 +++++ .../fluent/spec/BaseTaskItemListBuilder.java | 14 ++++++ .../fluent/spec/CallAgentAITaskBuilder.java | 43 +++++++++++++++++++ 6 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallAgentAITaskBuilder.java diff --git a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java index 2f461371..dfe2910c 100644 --- a/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java +++ b/experimental/ai/types/src/main/java/io/serverlessworkflow/ai/api/types/CallAgentAI.java @@ -24,22 +24,19 @@ public class CallAgentAI extends TaskBase { - private final AgentInstance instance; + private AgentInstance instance; - public CallAgentAI(Object instance) { - if (!(instance instanceof AgentInstance)) { - throw new IllegalArgumentException("Instance must be subtype of AgentInstance"); - } - - this.instance = (AgentInstance) instance; + public static Builder builder() { + return new Builder(); } public AgentInstance getAgentInstance() { return instance; } - public static Builder builder() { - return new Builder(); + public CallAgentAI setAgentInstance(Object object) { + this.instance = (AgentInstance) object; + return this; } public static class Builder { @@ -67,6 +64,10 @@ public CallAgentAI.Builder withOutputName(String outputName) { return this; } + public AgenticInnerBuilder withAgent(Object agent) { + return new AgenticInnerBuilder().withAgent(agent); + } + public CallAgentAI build() { Objects.requireNonNull(agentClass, "agentClass must be provided"); Objects.requireNonNull(chatModel, "chatModel must be provided"); @@ -76,11 +77,27 @@ public CallAgentAI build() { throw new IllegalArgumentException("outputName must not be blank"); } - return new CallAgentAI( + Object instance = AgentServices.agentBuilder(agentClass) .chatModel(chatModel) .outputName(outputName) - .build()); + .build(); + + return new AgenticInnerBuilder().withAgent(instance).build(); + } + + public static class AgenticInnerBuilder { + + private Object agent; + + public AgenticInnerBuilder withAgent(Object agent) { + this.agent = agent; + return this; + } + + public CallAgentAI build() { + return new CallAgentAI().setAgentInstance(agent); + } } } } diff --git a/fluent/pom.xml b/fluent/pom.xml index ff19f4d5..caff7919 100644 --- a/fluent/pom.xml +++ b/fluent/pom.xml @@ -35,6 +35,11 @@ serverlessworkflow-fluent-spec ${project.version} + + io.serverlessworkflow + serverlessworkflow-experimental-ai-types + ${project.version} + @@ -43,4 +48,4 @@ func - \ No newline at end of file + diff --git a/fluent/spec/pom.xml b/fluent/spec/pom.xml index 5ec62a98..fdd54289 100644 --- a/fluent/spec/pom.xml +++ b/fluent/spec/pom.xml @@ -27,6 +27,10 @@ junit-jupiter-api test + + io.serverlessworkflow + serverlessworkflow-experimental-ai-types + - \ No newline at end of file + diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java index d4c70aec..67eaf431 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java @@ -136,6 +136,16 @@ public TASK callHTTP(Consumer itemsConfigurer) { return self(); } + public TASK callAgentAI(String name, Consumer itemsConfigurer) { + taskItemListBuilder.callAgentAI(name, itemsConfigurer); + return self(); + } + + public TASK callAgentAI(Consumer itemsConfigurer) { + taskItemListBuilder.callAgentAI(itemsConfigurer); + return self(); + } + public DoTask build() { this.doTask.setDo(this.taskItemListBuilder.build()); return this.doTask; diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java index bb2a34dc..934c903b 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.fluent.spec; +import io.serverlessworkflow.ai.api.types.CallTaskAIChatModel; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; @@ -167,6 +168,19 @@ public SELF callHTTP(Consumer itemsConfigurer) { return this.callHTTP(UUID.randomUUID().toString(), itemsConfigurer); } + public SELF callAgentAI(String name, Consumer itemsConfigurer) { + requireNameAndConfig(name, itemsConfigurer); + final CallAgentAITaskBuilder callAgentAIBuilder = new CallAgentAITaskBuilder(); + itemsConfigurer.accept(callAgentAIBuilder); + return addTaskItem( + new TaskItem( + name, new Task().withCallTask(new CallTaskAIChatModel(callAgentAIBuilder.build())))); + } + + public SELF callAgentAI(Consumer itemsConfigurer) { + return this.callAgentAI(UUID.randomUUID().toString(), itemsConfigurer); + } + /** * @return an immutable snapshot of all {@link TaskItem}s added so far */ diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallAgentAITaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallAgentAITaskBuilder.java new file mode 100644 index 00000000..ef69342e --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/CallAgentAITaskBuilder.java @@ -0,0 +1,43 @@ +/* + * 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.fluent.spec; + +import io.serverlessworkflow.ai.api.types.CallAgentAI; + +public class CallAgentAITaskBuilder extends TaskBaseBuilder { + + private final CallAgentAI callAgentAI; + + CallAgentAITaskBuilder() { + callAgentAI = new CallAgentAI(); + super.setTask(this.callAgentAI); + } + + @Override + protected CallAgentAITaskBuilder self() { + return this; + } + + public CallAgentAITaskBuilder withAgent(Object agentInstance) { + this.callAgentAI.setAgentInstance(agentInstance); + return this; + } + + public CallAgentAI build() { + return callAgentAI; + } +}