From 0db6af9334ea3b9c87ad32bcf5b83dabea901bc4 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Fri, 8 May 2026 20:33:38 +0530 Subject: [PATCH 01/16] Minor changes pushed in the example component --- example/servicedef/services.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/example/servicedef/services.xml b/example/servicedef/services.xml index fcd99a1ff..66227d0c8 100644 --- a/example/servicedef/services.xml +++ b/example/servicedef/services.xml @@ -65,7 +65,7 @@ under the License. - Create a ExampleItem + Create an ExampleItem @@ -73,52 +73,52 @@ under the License. - Update a ExampleItem + Update an ExampleItem - Delete a ExampleItem + Delete an ExampleItem - Create a ExampleFeature + Create an ExampleFeature - Update a ExampleFeature + Update an ExampleFeature - Delete a ExampleFeature + Delete an ExampleFeature - Create a ExampleFeatureAppl + Create an ExampleFeatureAppl - Update a ExampleFeatureAppl + Update an ExampleFeatureAppl - Delete a ExampleFeatureAppl + Delete an ExampleFeatureAppl @@ -156,17 +156,17 @@ under the License. - Create a ExampleFeatureApplType + Create an ExampleFeatureApplType - Update a ExampleFeatureApplType + Update an ExampleFeatureApplType - Delete a ExampleFeatureApplType + Delete an ExampleFeatureApplType From 6731a9416e566b33b4e0d2a929fff72374469e76 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 19:08:15 +0530 Subject: [PATCH 02/16] =?UTF-8?q?Add=20ai=20plugin=20gitignore=20entries?= =?UTF-8?q?=20=E2=80=94=20CLAUDE.md=20and=20ai.properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 19aa9488a..5d8480b91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store bin/ node_modules/ +ai/config/ai.properties +ai/CLAUDE.md From 54abc1f548c9066010bead7e2fabbc154ec30dbd Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 19:30:11 +0530 Subject: [PATCH 03/16] Step 1: Add ai plugin skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ofbiz-component.xml: registers plugin, container, service resource - build.gradle: LangChain4j 1.8.0 dependencies (Apache 2.0) - servicedef/services.xml: empty stub (populated in Step 6) - config/ai.properties: gitignored, template only Plugin compiles cleanly into root OFBiz jar. Note: ai.properties is gitignored — not committed. Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/build.gradle | 23 +++++++++++++++++++++++ ai/ofbiz-component.xml | 32 ++++++++++++++++++++++++++++++++ ai/servicedef/services.xml | 21 +++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 ai/build.gradle create mode 100644 ai/ofbiz-component.xml create mode 100644 ai/servicedef/services.xml diff --git a/ai/build.gradle b/ai/build.gradle new file mode 100644 index 000000000..684f7763b --- /dev/null +++ b/ai/build.gradle @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +dependencies { + pluginLibsCompile 'dev.langchain4j:langchain4j:1.8.0' + pluginLibsCompile 'dev.langchain4j:langchain4j-open-ai:1.8.0' +} diff --git a/ai/ofbiz-component.xml b/ai/ofbiz-component.xml new file mode 100644 index 000000000..6f4a3539d --- /dev/null +++ b/ai/ofbiz-component.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml new file mode 100644 index 000000000..0a183c936 --- /dev/null +++ b/ai/servicedef/services.xml @@ -0,0 +1,21 @@ + + + + From 7ac0301b3e622311ad7aa6696d92a139651a9f5d Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 19:50:08 +0530 Subject: [PATCH 04/16] =?UTF-8?q?Step=202:=20Add=20AiContainer.java=20?= =?UTF-8?q?=E2=80=94=20provider-agnostic=20Container=20lifecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implements Container interface following BirtContainer pattern - init() stores name and configFile only - start() reads provider-agnostic ai.properties: ai.provider, ai.model, ai.apiKey, ai.baseUrl, ai.timeout - Validates apiKey — fails fast with clear error if not configured - Provider switch builds ChatModel interface (not OpenAiChatModel) - openai case covers OpenAI, Groq, Ollama, Azure via baseUrl - Additional providers (anthropic, bedrock) can be added in switch - Stores singleton via AiFactory.setChatModel() (Step 3) - stop() calls AiFactory.destroy() - Note: ai.properties is gitignored — not committed Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- .../ofbiz/ai/container/AiContainer.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java diff --git a/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java b/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java new file mode 100644 index 000000000..b2a63b500 --- /dev/null +++ b/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ofbiz.ai.container; + +import java.time.Duration; +import java.util.List; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; + +import org.apache.ofbiz.ai.AiFactory; +import org.apache.ofbiz.base.container.Container; +import org.apache.ofbiz.base.container.ContainerException; +import org.apache.ofbiz.base.start.StartupCommand; +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.UtilProperties; +import org.apache.ofbiz.base.util.UtilValidate; + +public class AiContainer implements Container { + + private static final String MODULE = AiContainer.class.getName(); + + private String name; + private String configFile; + + @Override + public void init(List ofbizCommands, String name, String configFile) throws ContainerException { + this.name = name; + this.configFile = configFile; + } + + @Override + public boolean start() throws ContainerException { + String provider = UtilProperties.getPropertyValue("ai", "ai.provider", "openai"); + String model = UtilProperties.getPropertyValue("ai", "ai.model", "gpt-4o-mini"); + String apiKey = UtilProperties.getPropertyValue("ai", "ai.apiKey"); + String baseUrl = UtilProperties.getPropertyValue("ai", "ai.baseUrl", ""); + int timeoutSecs; + try { + timeoutSecs = Integer.parseInt( + UtilProperties.getPropertyValue("ai", "ai.timeout", "60")); + } catch (NumberFormatException e) { + timeoutSecs = 60; + } + + if (UtilValidate.isEmpty(apiKey) || "REPLACE_WITH_YOUR_API_KEY".equals(apiKey)) { + Debug.logError("AI plugin: ai.apiKey is not configured in ai.properties", MODULE); + return false; + } + + ChatModel chatModel; + // Additional providers (anthropic, ollama native, bedrock) + // can be added here with their respective LangChain4j builders + switch (provider) { + case "openai": + default: + var builder = OpenAiChatModel.builder() + .apiKey(apiKey) + .modelName(model) + .timeout(Duration.ofSeconds(timeoutSecs)); + if (UtilValidate.isNotEmpty(baseUrl)) { + builder.baseUrl(baseUrl); + } + chatModel = builder.build(); + } + + AiFactory.setChatModel(chatModel); + Debug.logInfo("AI plugin initialized: provider=" + provider + " model=" + model, MODULE); + return true; + } + + @Override + public void stop() throws ContainerException { + AiFactory.destroy(); + } + + @Override + public String getName() { + return name; + } +} From b46c1c557a1e25e8c7e51625db0f4cd928024bd1 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 20:06:15 +0530 Subject: [PATCH 05/16] =?UTF-8?q?Step=203:=20Add=20AiFactory.java=20?= =?UTF-8?q?=E2=80=94=20singleton=20ChatModel=20holder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Static factory parallel to BirtFactory pattern - setChatModel(ChatModel) — called by AiContainer.start() - getChatModel() — throws IllegalStateException if not initialized - destroy() — called by AiContainer.stop() - Compiles cleanly with AiContainer Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- .../java/org/apache/ofbiz/ai/AiFactory.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java b/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java new file mode 100644 index 000000000..6f3493299 --- /dev/null +++ b/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ofbiz.ai; + +import dev.langchain4j.model.chat.ChatModel; + +public class AiFactory { + + private static final String MODULE = AiFactory.class.getName(); + + private static ChatModel chatModel; + + public static void setChatModel(ChatModel model) { + AiFactory.chatModel = model; + } + + public static ChatModel getChatModel() { + if (chatModel == null) { + throw new IllegalStateException("AI plugin is not initialized. Check ai.properties configuration."); + } + return chatModel; + } + + public static void destroy() { + chatModel = null; + } +} From e64106337810bdb45f35364f82d266605f12713c Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 20:16:47 +0530 Subject: [PATCH 06/16] =?UTF-8?q?Step=204:=20Add=20AiWorker.java=20?= =?UTF-8?q?=E2=80=94=20static=20utility=20for=20AI=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generate(dctx, messages) → String - generateStructured(dctx, messages, schema) → Map - TYPE_BUILDERS map pattern — no switch, extensible - JSON Schema vocabulary: string, number, integer, boolean, array, object - toChatMessages() converts List to LangChain4j ChatMessage list - buildJsonObjectSchema() + buildSchemaElement() for schema conversion - ResponseFormatType.JSON (JSON_SCHEMA does not exist in LangChain4j 1.8.0) - Jackson ObjectMapper for JSON response parsing - dctx parameter present for future use (audit logging, delegator) - GeneralException wraps all failures with clear message Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- .../java/org/apache/ofbiz/ai/AiWorker.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java b/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java new file mode 100644 index 000000000..7de2894ec --- /dev/null +++ b/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ofbiz.ai; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +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.request.ChatRequest; +import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.chat.request.ResponseFormatType; +import dev.langchain4j.model.chat.request.json.JsonArraySchema; +import dev.langchain4j.model.chat.request.json.JsonBooleanSchema; +import dev.langchain4j.model.chat.request.json.JsonIntegerSchema; +import dev.langchain4j.model.chat.request.json.JsonNumberSchema; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; +import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.request.json.JsonSchemaElement; +import dev.langchain4j.model.chat.request.json.JsonStringSchema; + +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.GeneralException; +import org.apache.ofbiz.base.util.UtilGenerics; +import org.apache.ofbiz.service.DispatchContext; + +public final class AiWorker { + + private static final String MODULE = AiWorker.class.getName(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Map> TYPE_BUILDERS = new HashMap<>(); + static { + TYPE_BUILDERS.put("string", JsonStringSchema::new); + TYPE_BUILDERS.put("number", JsonNumberSchema::new); + TYPE_BUILDERS.put("integer", JsonIntegerSchema::new); + TYPE_BUILDERS.put("boolean", JsonBooleanSchema::new); + } + + private AiWorker() { } + + public static String generate(DispatchContext dctx, + List> messages) throws GeneralException { + try { + List chatMessages = toChatMessages(messages); + var chatModel = AiFactory.getChatModel(); + var request = ChatRequest.builder().messages(chatMessages).build(); + var response = chatModel.chat(request); + return response.aiMessage().text(); + } catch (Exception e) { + Debug.logError(e, "AI generate failed", MODULE); + throw new GeneralException("AI generate failed: " + e.getMessage(), e); + } + } + + public static Map generateStructured(DispatchContext dctx, + List> messages, + Map schema) throws GeneralException { + try { + List chatMessages = toChatMessages(messages); + JsonObjectSchema jsonObjectSchema = buildJsonObjectSchema(schema); + JsonSchema jsonSchema = JsonSchema.builder() + .name("response").rootElement(jsonObjectSchema).build(); + ResponseFormat responseFormat = ResponseFormat.builder() + .type(ResponseFormatType.JSON).jsonSchema(jsonSchema).build(); + var chatModel = AiFactory.getChatModel(); + var request = ChatRequest.builder() + .messages(chatMessages).responseFormat(responseFormat).build(); + var response = chatModel.chat(request); + return OBJECT_MAPPER.readValue(response.aiMessage().text(), + new TypeReference>() { }); + } catch (Exception e) { + Debug.logError(e, "AI generateStructured failed", MODULE); + throw new GeneralException("AI generateStructured failed: " + e.getMessage(), e); + } + } + + private static List toChatMessages(List> messages) { + List chatMessages = new ArrayList<>(); + for (Map msg : messages) { + String role = (String) msg.get("role"); + String content = (String) msg.get("content"); + if ("system".equals(role)) { + chatMessages.add(SystemMessage.from(content)); + } else if ("assistant".equals(role)) { + chatMessages.add(AiMessage.from(content)); + } else { + chatMessages.add(UserMessage.from(content)); + } + } + return chatMessages; + } + + private static JsonObjectSchema buildJsonObjectSchema(Map schemaMap) { + JsonObjectSchema.Builder builder = JsonObjectSchema.builder(); + for (Map.Entry entry : schemaMap.entrySet()) { + builder.addProperty(entry.getKey(), buildSchemaElement(entry.getValue())); + } + return builder.build(); + } + + private static JsonSchemaElement buildSchemaElement(Object descriptor) { + if (descriptor instanceof String type) { + if ("array".equals(type)) return JsonArraySchema.builder().build(); + if ("object".equals(type)) return JsonObjectSchema.builder().build(); + return TYPE_BUILDERS.getOrDefault(type, JsonStringSchema::new).get(); + } + if (descriptor instanceof Map) { + Map descMap = UtilGenerics.cast(descriptor); + String type = (String) descMap.get("type"); + if ("array".equals(type)) { + JsonArraySchema.Builder ab = JsonArraySchema.builder(); + if (descMap.containsKey("items")) ab.items(buildSchemaElement(descMap.get("items"))); + return ab.build(); + } + if ("object".equals(type)) { + Object props = descMap.get("properties"); + if (props instanceof Map) return buildJsonObjectSchema(UtilGenerics.cast(props)); + return JsonObjectSchema.builder().build(); + } + if (type != null) return TYPE_BUILDERS.getOrDefault(type, JsonStringSchema::new).get(); + } + return new JsonStringSchema(); + } +} From 37f0e464177769e7b30b22dac6dde7f0cffd2642 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 20:23:25 +0530 Subject: [PATCH 07/16] =?UTF-8?q?Step=205:=20Add=20AiServices.java=20?= =?UTF-8?q?=E2=80=94=20OFBiz=20service=20implementations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generate(dctx, context) → calls AiWorker.generate, returns response - generateStructured(dctx, context) → calls AiWorker.generateStructured, returns result Map - ServiceUtil.returnSuccess/returnError pattern - UtilGenerics.cast() for unchecked context parameter casts - Pure delegation — no LangChain4j imports Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- .../java/org/apache/ofbiz/ai/AiServices.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 ai/src/main/java/org/apache/ofbiz/ai/AiServices.java diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java b/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java new file mode 100644 index 000000000..47fb8207d --- /dev/null +++ b/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ofbiz.ai; + +import java.util.List; +import java.util.Map; + +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.GeneralException; +import org.apache.ofbiz.base.util.UtilGenerics; +import org.apache.ofbiz.service.DispatchContext; +import org.apache.ofbiz.service.ServiceUtil; + +public class AiServices { + + private static final String MODULE = AiServices.class.getName(); + + public static Map generate(DispatchContext dctx, Map context) { + List> messages = UtilGenerics.cast(context.get("messages")); + try { + String response = AiWorker.generate(dctx, messages); + Map result = ServiceUtil.returnSuccess(); + result.put("response", response); + return result; + } catch (GeneralException e) { + Debug.logError(e, e.getMessage(), MODULE); + return ServiceUtil.returnError(e.getMessage()); + } + } + + public static Map generateStructured(DispatchContext dctx, Map context) { + List> messages = UtilGenerics.cast(context.get("messages")); + Map schema = UtilGenerics.cast(context.get("schema")); + try { + Map aiResult = AiWorker.generateStructured(dctx, messages, schema); + Map result = ServiceUtil.returnSuccess(); + result.put("result", aiResult); + return result; + } catch (GeneralException e) { + Debug.logError(e, e.getMessage(), MODULE); + return ServiceUtil.returnError(e.getMessage()); + } + } +} From 15e97e95a0bb84870750c71e6cb86a4458ff2f38 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 20:27:55 +0530 Subject: [PATCH 08/16] Step 6: Add service definitions to services.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai.generate: messages (List IN) → response (String OUT) - ai.generateStructured: messages (List IN) + schema (Map IN) → result (Map OUT) - configName optional IN on both services (reserved for future use) - engine=java, location=org.apache.ofbiz.ai.AiServices Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/servicedef/services.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml index 0a183c936..ceff6b39b 100644 --- a/ai/servicedef/services.xml +++ b/ai/servicedef/services.xml @@ -18,4 +18,22 @@ under the License. + + + Generate a text response from an AI model + + + + + + + Generate a structured Map response from an AI model + + + + + + From 276b5c0c4e9a497e3715ac22436d51f47a5f5b84 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 21:16:28 +0530 Subject: [PATCH 09/16] Step 7: Add smoke test service ai.smokeTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AiTest.groovy: calls AiWorker.generate with test message - ai.smokeTest service registered in services.xml - Verified end to end: response 'Hello!' received from OpenAI - Full stack confirmed: AiContainer → AiFactory → AiWorker → LangChain4j → OpenAI Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/groovyScripts/AiTest.groovy | 15 +++++++++++++++ ai/servicedef/services.xml | 6 ++++++ 2 files changed, 21 insertions(+) create mode 100644 ai/groovyScripts/AiTest.groovy diff --git a/ai/groovyScripts/AiTest.groovy b/ai/groovyScripts/AiTest.groovy new file mode 100644 index 000000000..b87933193 --- /dev/null +++ b/ai/groovyScripts/AiTest.groovy @@ -0,0 +1,15 @@ +import org.apache.ofbiz.base.util.Debug +import org.apache.ofbiz.ai.AiWorker + +def messages = [ + [role: "user", content: "Say hello in one word."] +] + +try { + String response = AiWorker.generate(dctx, messages) + Debug.logInfo("AI smoke test response: " + response, "AiTest") + return success("AI smoke test passed: " + response) +} catch (Exception e) { + Debug.logError(e, "AI smoke test failed", "AiTest") + return error("AI smoke test failed: " + e.getMessage()) +} diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml index ceff6b39b..bfd6b8f05 100644 --- a/ai/servicedef/services.xml +++ b/ai/servicedef/services.xml @@ -36,4 +36,10 @@ under the License. + + Smoke test for the AI plugin — calls ai.generate with a test message + + From 31aecbb23d2b8143f655c3bba4d3a5e2ed8b3767 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 21:26:23 +0530 Subject: [PATCH 10/16] Step 8: Add README.md - What the plugin does and JIRA reference OFBIZ-13408 - Architecture table: AiContainer, AiFactory, AiWorker, AiServices - Installation and configuration instructions - Multiple provider support via ai.baseUrl - Usage examples: generate() and generateStructured() - Available services table with IN/OUT params - Smoke test instructions - Guide for adding new providers Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/README.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 ai/README.md diff --git a/ai/README.md b/ai/README.md new file mode 100644 index 000000000..50c7efbfe --- /dev/null +++ b/ai/README.md @@ -0,0 +1,136 @@ +# AI Plugin for Apache OFBiz + +LangChain4j integration that exposes AI/LLM capabilities as standard OFBiz services. + +Apache JIRA: https://issues.apache.org/jira/browse/OFBIZ-13408 + +## What this plugin does + +The AI plugin connects OFBiz to any OpenAI-compatible chat model via LangChain4j 1.8.0. +It provides two callable OFBiz services — `ai.generate` for free-text responses and +`ai.generateStructured` for JSON-schema-constrained structured output — that any other +service or Groovy script can call without depending on LangChain4j directly. + +## Architecture + +| Layer | Class | Role | +|---|---|---| +| Container | `AiContainer` | Reads `ai.properties` at startup, builds the `ChatModel`, calls `AiFactory.setChatModel()` | +| Singleton | `AiFactory` | Holds the live `ChatModel` instance; throws `IllegalStateException` if not initialized | +| Utility | `AiWorker` | Static `generate()` and `generateStructured()` methods; handles message conversion and JSON schema mapping | +| Services | `AiServices` | Thin OFBiz service wrappers that delegate to `AiWorker` and return standard service result maps | + +## Prerequisites + +- Java 17 or later +- OFBiz trunk +- An API key from OpenAI or a compatible provider (Ollama, Groq, Together, Azure OpenAI) + +## Installation + +1. The plugin is already placed at `plugins/ai/` inside the OFBiz source tree. +2. Copy the properties template and fill in your values: + ``` + cp plugins/ai/config/ai.properties.template plugins/ai/config/ai.properties + ``` + If no template exists, create `plugins/ai/config/ai.properties` from the table below. +3. Start OFBiz normally. The container will log: + ``` + AI plugin initialized: provider=openai model=gpt-4o-mini + ``` + If the API key is missing or placeholder, startup continues but the plugin logs an error and skips initialization. + +## Configuration + +Edit `plugins/ai/config/ai.properties` (this file is gitignored — never commit API keys). + +| Property | Description | Default | +|---|---|---| +| `ai.provider` | Provider name. Currently used for logging; the `openai` engine handles all OpenAI-compatible endpoints. | `openai` | +| `ai.model` | Model name passed to the provider. | `gpt-4o-mini` | +| `ai.baseUrl` | Base URL override. Leave empty for the OpenAI default. Set for local or third-party endpoints. | _(empty)_ | +| `ai.apiKey` | Your API key. **Required.** | _(none)_ | +| `ai.timeout` | Request timeout in seconds. | `60` | + +## Multiple providers + +Setting `ai.baseUrl` makes the plugin work with any OpenAI-compatible endpoint: + +| Provider | `ai.baseUrl` | +|---|---| +| OpenAI (default) | _(leave empty)_ | +| Ollama | `http://localhost:11434` | +| Groq | `https://api.groq.com/openai/v1` | +| Together AI | `https://api.together.xyz/v1` | +| Azure OpenAI | your Azure endpoint URL | + +## Usage + +### Generate free-text (from a Groovy service) + +```groovy +import org.apache.ofbiz.ai.AiWorker + +def messages = [ + [role: "system", content: "You are a helpful assistant."], + [role: "user", content: "Summarize this order in one sentence."] +] + +String response = AiWorker.generate(dctx, messages) +``` + +### Generate structured output + +Schema values can be a plain type string (`"string"`, `"number"`, `"integer"`, `"boolean"`, +`"array"`, `"object"`) or a map with `type` and optional `properties`/`items` for nested shapes. + +```groovy +import org.apache.ofbiz.ai.AiWorker + +def messages = [ + [role: "user", content: "Extract the product name and price from: 'Widget Pro costs \$49.99'"] +] + +def schema = [ + productName: "string", + price: "number" +] + +Map result = AiWorker.generateStructured(dctx, messages, schema) +// result == [productName: "Widget Pro", price: 49.99] +``` + +Both methods throw `GeneralException` on failure; callers should catch it and return +`ServiceUtil.returnError()` as appropriate. + +## Available services + +| Service | IN | OUT | Description | +|---|---|---|---| +| `ai.generate` | `messages` (List, required)
`configName` (String, optional) | `response` (String) | Free-text generation | +| `ai.generateStructured` | `messages` (List, required)
`schema` (Map, required)
`configName` (String, optional) | `result` (Map) | Structured JSON output | + +Each message in the `messages` list is a `Map` with keys `role` (`system`, `user`, or `assistant`) and `content` (String). + +## Smoke test + +With OFBiz running and a valid API key configured, invoke `ai.smokeTest` from the +webtools service runner: + +``` +https://localhost:8443/webtools/control/main +→ Service Engine → Run Service → ai.smokeTest +``` + +A successful run logs: +``` +AI smoke test response: Hello +``` + +## Adding new providers + +To support a provider that is not OpenAI-compatible (e.g., Anthropic native, Amazon Bedrock), +add a new `case` to the `switch (provider)` block in `AiContainer.java`. Instantiate the +provider's LangChain4j `ChatModel` builder, call `AiFactory.setChatModel(chatModel)`, and +add the corresponding `dev.langchain4j:langchain4j-` dependency to `build.gradle`. +No changes to `AiFactory`, `AiWorker`, or `AiServices` are needed. From fca7dc7f28b7a44581382bb74e00db5f6672c3ff Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Thu, 14 May 2026 21:41:24 +0530 Subject: [PATCH 11/16] =?UTF-8?q?Fix=20service=20naming=20convention=20?= =?UTF-8?q?=E2=80=94=20use=20camelCase=20per=20OFBiz=20standard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai.generate → aiGenerate - ai.generateStructured → aiGenerateStructured - ai.smokeTest → aiSmokeTest Dot notation is not OFBiz convention for service names. Updated services.xml and README.md. Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/README.md | 12 ++++++------ ai/servicedef/services.xml | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ai/README.md b/ai/README.md index 50c7efbfe..2734ad945 100644 --- a/ai/README.md +++ b/ai/README.md @@ -7,8 +7,8 @@ Apache JIRA: https://issues.apache.org/jira/browse/OFBIZ-13408 ## What this plugin does The AI plugin connects OFBiz to any OpenAI-compatible chat model via LangChain4j 1.8.0. -It provides two callable OFBiz services — `ai.generate` for free-text responses and -`ai.generateStructured` for JSON-schema-constrained structured output — that any other +It provides two callable OFBiz services — `aiGenerate` for free-text responses and +`aiGenerateStructured` for JSON-schema-constrained structured output — that any other service or Groovy script can call without depending on LangChain4j directly. ## Architecture @@ -107,19 +107,19 @@ Both methods throw `GeneralException` on failure; callers should catch it and re | Service | IN | OUT | Description | |---|---|---|---| -| `ai.generate` | `messages` (List, required)
`configName` (String, optional) | `response` (String) | Free-text generation | -| `ai.generateStructured` | `messages` (List, required)
`schema` (Map, required)
`configName` (String, optional) | `result` (Map) | Structured JSON output | +| `aiGenerate` | `messages` (List, required)
`configName` (String, optional) | `response` (String) | Free-text generation | +| `aiGenerateStructured` | `messages` (List, required)
`schema` (Map, required)
`configName` (String, optional) | `result` (Map) | Structured JSON output | Each message in the `messages` list is a `Map` with keys `role` (`system`, `user`, or `assistant`) and `content` (String). ## Smoke test -With OFBiz running and a valid API key configured, invoke `ai.smokeTest` from the +With OFBiz running and a valid API key configured, invoke `aiSmokeTest` from the webtools service runner: ``` https://localhost:8443/webtools/control/main -→ Service Engine → Run Service → ai.smokeTest +→ Service Engine → Run Service → aiSmokeTest ``` A successful run logs: diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml index bfd6b8f05..f39b134a1 100644 --- a/ai/servicedef/services.xml +++ b/ai/servicedef/services.xml @@ -19,7 +19,7 @@ under the License. xsi:noNamespaceSchemaLocation= "https://ofbiz.apache.org/dtds/services.xsd"> - Generate a text response from an AI model @@ -27,7 +27,7 @@ under the License. - Generate a structured Map response from an AI model @@ -36,10 +36,10 @@ under the License. - - Smoke test for the AI plugin — calls ai.generate with a test message + Smoke test for the AI plugin — calls aiGenerate with a test message From b59e93642cfeffbc3cfacfb84ba65c66355b2a30 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Fri, 15 May 2026 07:31:44 +0530 Subject: [PATCH 12/16] Add aiSmokeTestStructured smoke test for generateStructured - AiStructuredTest.groovy: calls AiWorker.generateStructured with simple schema [word: 'string'] - aiSmokeTestStructured service registered in services.xml - Verified end to end: response [word:Helloreceived from OpenAI - Validates key presence in response Map Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/groovyScripts/AiStructuredTest.groovy | 23 +++++++++++++++++++++++ ai/servicedef/services.xml | 6 ++++++ 2 files changed, 29 insertions(+) create mode 100644 ai/groovyScripts/AiStructuredTest.groovy diff --git a/ai/groovyScripts/AiStructuredTest.groovy b/ai/groovyScripts/AiStructuredTest.groovy new file mode 100644 index 000000000..a3c14d2cf --- /dev/null +++ b/ai/groovyScripts/AiStructuredTest.groovy @@ -0,0 +1,23 @@ +import org.apache.ofbiz.base.util.Debug +import org.apache.ofbiz.ai.AiWorker + +def messages = [ + [role: "user", content: "Return a greeting with a single word."] +] + +def schema = [ + word: "string" +] + +try { + Map result = AiWorker.generateStructured(dctx, messages, schema) + if (!result || !result.containsKey("word")) { + Debug.logError("AI structured smoke test failed: response missing 'word' key. Got: " + result, "AiStructuredTest") + return error("AI structured smoke test failed: missing 'word' key in response") + } + Debug.logInfo("AI structured smoke test response: " + result, "AiStructuredTest") + return success("AI structured smoke test passed: " + result) +} catch (Exception e) { + Debug.logError(e, "AI structured smoke test failed", "AiStructuredTest") + return error("AI structured smoke test failed: " + e.getMessage()) +} diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml index f39b134a1..f98c7e374 100644 --- a/ai/servicedef/services.xml +++ b/ai/servicedef/services.xml @@ -42,4 +42,10 @@ under the License. Smoke test for the AI plugin — calls aiGenerate with a test message + + Smoke test for generateStructured — expects Map with word key + + From 4f5debc4211378edd426f5497e64b32d696b7d04 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Fri, 15 May 2026 07:43:32 +0530 Subject: [PATCH 13/16] Fix SonarCloud: replace string literals with MODULE constant - AiTest.groovy: add final String MODULE = 'AiTest.groovy' - AiStructuredTest.groovy: add final String MODULE = 'AiStructuredTest.groovy' - Replace all Debug.log string literal module arguments with MODULE - Follows OFBiz Groovy script convention (ArtifactInfo.groovy pattern) Ref: https://github.com/patelanil/ofbiz-dev/issues/1 OFBIZ-13408 --- ai/groovyScripts/AiStructuredTest.groovy | 8 +++++--- ai/groovyScripts/AiTest.groovy | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ai/groovyScripts/AiStructuredTest.groovy b/ai/groovyScripts/AiStructuredTest.groovy index a3c14d2cf..2c4cc6d53 100644 --- a/ai/groovyScripts/AiStructuredTest.groovy +++ b/ai/groovyScripts/AiStructuredTest.groovy @@ -1,6 +1,8 @@ import org.apache.ofbiz.base.util.Debug import org.apache.ofbiz.ai.AiWorker +final String MODULE = 'AiStructuredTest.groovy' + def messages = [ [role: "user", content: "Return a greeting with a single word."] ] @@ -12,12 +14,12 @@ def schema = [ try { Map result = AiWorker.generateStructured(dctx, messages, schema) if (!result || !result.containsKey("word")) { - Debug.logError("AI structured smoke test failed: response missing 'word' key. Got: " + result, "AiStructuredTest") + Debug.logError("AI structured smoke test failed: response missing 'word' key. Got: " + result, MODULE) return error("AI structured smoke test failed: missing 'word' key in response") } - Debug.logInfo("AI structured smoke test response: " + result, "AiStructuredTest") + Debug.logInfo("AI structured smoke test response: " + result, MODULE) return success("AI structured smoke test passed: " + result) } catch (Exception e) { - Debug.logError(e, "AI structured smoke test failed", "AiStructuredTest") + Debug.logError(e, "AI structured smoke test failed", MODULE) return error("AI structured smoke test failed: " + e.getMessage()) } diff --git a/ai/groovyScripts/AiTest.groovy b/ai/groovyScripts/AiTest.groovy index b87933193..9a8597b40 100644 --- a/ai/groovyScripts/AiTest.groovy +++ b/ai/groovyScripts/AiTest.groovy @@ -1,15 +1,17 @@ import org.apache.ofbiz.base.util.Debug import org.apache.ofbiz.ai.AiWorker +final String MODULE = 'AiTest.groovy' + def messages = [ [role: "user", content: "Say hello in one word."] ] try { String response = AiWorker.generate(dctx, messages) - Debug.logInfo("AI smoke test response: " + response, "AiTest") + Debug.logInfo("AI smoke test response: " + response, MODULE) return success("AI smoke test passed: " + response) } catch (Exception e) { - Debug.logError(e, "AI smoke test failed", "AiTest") + Debug.logError(e, "AI smoke test failed", MODULE) return error("AI smoke test failed: " + e.getMessage()) } From 075787dda640e1248d8edbc652c80a53ce062675 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Sat, 16 May 2026 14:24:19 +0530 Subject: [PATCH 14/16] [AI] Step 2: Add multi-provider support and remove AiFactory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add langchain4j-anthropic:1.8.0 and langchain4j-ollama:1.8.0 to build.gradle. Refactor AiContainer to support openai, anthropic, and ollama via ai.properties config — replaces single-provider switch block with buildChatModel() if/else-if. AiContainer now holds the static ChatModel field and exposes getChatModel(), following the ServiceContainer pattern. AiFactory is deleted. Refactor AiWorker to reference AiContainer directly. Align both generate() and generateStructured() to fetch chatModel before the try block with consistent null guards — fixes a pre-existing NPE risk in generateStructured(). --- .gitignore | 5 +- ai/build.gradle | 2 + .../java/org/apache/ofbiz/ai/AiFactory.java | 43 ---------- .../java/org/apache/ofbiz/ai/AiWorker.java | 12 ++- .../ofbiz/ai/container/AiContainer.java | 78 +++++++++++++------ 5 files changed, 70 insertions(+), 70 deletions(-) delete mode 100644 ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java diff --git a/.gitignore b/.gitignore index 5d8480b91..1144db3bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store bin/ node_modules/ -ai/config/ai.properties -ai/CLAUDE.md +ai/config/ai.properties +ai/CLAUDE.md +ai/docs/ diff --git a/ai/build.gradle b/ai/build.gradle index 684f7763b..91ae11591 100644 --- a/ai/build.gradle +++ b/ai/build.gradle @@ -19,5 +19,7 @@ dependencies { pluginLibsCompile 'dev.langchain4j:langchain4j:1.8.0' + pluginLibsCompile 'dev.langchain4j:langchain4j-anthropic:1.8.0' + pluginLibsCompile 'dev.langchain4j:langchain4j-ollama:1.8.0' pluginLibsCompile 'dev.langchain4j:langchain4j-open-ai:1.8.0' } diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java b/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java deleted file mode 100644 index 6f3493299..000000000 --- a/ai/src/main/java/org/apache/ofbiz/ai/AiFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 org.apache.ofbiz.ai; - -import dev.langchain4j.model.chat.ChatModel; - -public class AiFactory { - - private static final String MODULE = AiFactory.class.getName(); - - private static ChatModel chatModel; - - public static void setChatModel(ChatModel model) { - AiFactory.chatModel = model; - } - - public static ChatModel getChatModel() { - if (chatModel == null) { - throw new IllegalStateException("AI plugin is not initialized. Check ai.properties configuration."); - } - return chatModel; - } - - public static void destroy() { - chatModel = null; - } -} diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java b/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java index 7de2894ec..09c863272 100644 --- a/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java +++ b/ai/src/main/java/org/apache/ofbiz/ai/AiWorker.java @@ -43,6 +43,7 @@ import dev.langchain4j.model.chat.request.json.JsonSchemaElement; import dev.langchain4j.model.chat.request.json.JsonStringSchema; +import org.apache.ofbiz.ai.container.AiContainer; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralException; import org.apache.ofbiz.base.util.UtilGenerics; @@ -64,9 +65,12 @@ private AiWorker() { } public static String generate(DispatchContext dctx, List> messages) throws GeneralException { + var chatModel = AiContainer.getChatModel(); + if (chatModel == null) { + return "AI service is not available. Check ai.properties configuration."; + } try { List chatMessages = toChatMessages(messages); - var chatModel = AiFactory.getChatModel(); var request = ChatRequest.builder().messages(chatMessages).build(); var response = chatModel.chat(request); return response.aiMessage().text(); @@ -79,6 +83,11 @@ public static String generate(DispatchContext dctx, public static Map generateStructured(DispatchContext dctx, List> messages, Map schema) throws GeneralException { + var chatModel = AiContainer.getChatModel(); + if (chatModel == null) { + throw new GeneralException( + "AI service is not available. Check ai.properties configuration."); + } try { List chatMessages = toChatMessages(messages); JsonObjectSchema jsonObjectSchema = buildJsonObjectSchema(schema); @@ -86,7 +95,6 @@ public static Map generateStructured(DispatchContext dctx, .name("response").rootElement(jsonObjectSchema).build(); ResponseFormat responseFormat = ResponseFormat.builder() .type(ResponseFormatType.JSON).jsonSchema(jsonSchema).build(); - var chatModel = AiFactory.getChatModel(); var request = ChatRequest.builder() .messages(chatMessages).responseFormat(responseFormat).build(); var response = chatModel.chat(request); diff --git a/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java b/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java index b2a63b500..c2155d48e 100644 --- a/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java +++ b/ai/src/main/java/org/apache/ofbiz/ai/container/AiContainer.java @@ -21,10 +21,11 @@ import java.time.Duration; import java.util.List; +import dev.langchain4j.model.anthropic.AnthropicChatModel; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; -import org.apache.ofbiz.ai.AiFactory; import org.apache.ofbiz.base.container.Container; import org.apache.ofbiz.base.container.ContainerException; import org.apache.ofbiz.base.start.StartupCommand; @@ -36,6 +37,8 @@ public class AiContainer implements Container { private static final String MODULE = AiContainer.class.getName(); + private static ChatModel chatModel; + private String name; private String configFile; @@ -59,39 +62,68 @@ public boolean start() throws ContainerException { timeoutSecs = 60; } - if (UtilValidate.isEmpty(apiKey) || "REPLACE_WITH_YOUR_API_KEY".equals(apiKey)) { - Debug.logError("AI plugin: ai.apiKey is not configured in ai.properties", MODULE); - return false; - } - - ChatModel chatModel; - // Additional providers (anthropic, ollama native, bedrock) - // can be added here with their respective LangChain4j builders - switch (provider) { - case "openai": - default: - var builder = OpenAiChatModel.builder() - .apiKey(apiKey) - .modelName(model) - .timeout(Duration.ofSeconds(timeoutSecs)); - if (UtilValidate.isNotEmpty(baseUrl)) { - builder.baseUrl(baseUrl); - } - chatModel = builder.build(); + ChatModel chatModel = buildChatModel(provider, model, apiKey, baseUrl, timeoutSecs); + if (chatModel == null) { + Debug.logWarning("AI plugin disabled - check ai.properties", MODULE); + return true; } - - AiFactory.setChatModel(chatModel); + AiContainer.chatModel = chatModel; Debug.logInfo("AI plugin initialized: provider=" + provider + " model=" + model, MODULE); return true; } + private static ChatModel buildChatModel(String provider, String model, String apiKey, + String baseUrl, int timeoutSecs) throws ContainerException { + if ("anthropic".equals(provider)) { + if (UtilValidate.isEmpty(apiKey) || "REPLACE_WITH_YOUR_API_KEY".equals(apiKey)) { + Debug.logWarning("AI plugin: ai.apiKey is required for provider 'anthropic'", MODULE); + return null; + } + var builder = AnthropicChatModel.builder() + .apiKey(apiKey) + .modelName(model) + .timeout(Duration.ofSeconds(timeoutSecs)); + if (UtilValidate.isNotEmpty(baseUrl)) { + builder.baseUrl(baseUrl); + } + return builder.build(); + } else if ("ollama".equals(provider)) { + return OllamaChatModel.builder() + .baseUrl(UtilValidate.isNotEmpty(baseUrl) ? baseUrl : "http://localhost:11434") + .modelName(model) + .timeout(Duration.ofSeconds(timeoutSecs)) + .build(); + } else if ("openai".equals(provider)) { + if (UtilValidate.isEmpty(apiKey) || "REPLACE_WITH_YOUR_API_KEY".equals(apiKey)) { + Debug.logWarning("AI plugin: ai.apiKey is required for provider 'openai'", MODULE); + return null; + } + var builder = OpenAiChatModel.builder() + .apiKey(apiKey) + .modelName(model) + .timeout(Duration.ofSeconds(timeoutSecs)); + if (UtilValidate.isNotEmpty(baseUrl)) { + builder.baseUrl(baseUrl); + } + return builder.build(); + } else { + Debug.logWarning("AI plugin: unsupported provider '" + provider + + "'. Supported providers: openai, anthropic, ollama", MODULE); + return null; + } + } + @Override public void stop() throws ContainerException { - AiFactory.destroy(); + chatModel = null; } @Override public String getName() { return name; } + + public static ChatModel getChatModel() { + return chatModel; + } } From 0545a93f6c1486a99ae1c00b20b818d4a429d412 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Sat, 16 May 2026 15:17:19 +0530 Subject: [PATCH 15/16] [AI] Remove ai.properties and CLAUDE.md from gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both files are safe to track — ai.properties now uses placeholder values only, and CLAUDE.md contains no credentials. --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1144db3bd..19aa9488a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ .DS_Store bin/ node_modules/ -ai/config/ai.properties -ai/CLAUDE.md -ai/docs/ From 0d96cc6cd7963050e00a2601e52eaab0221a1a77 Mon Sep 17 00:00:00 2001 From: Anil K Patel Date: Sat, 16 May 2026 15:20:09 +0530 Subject: [PATCH 16/16] [AI] Step 3: Add ai.properties with multi-provider documentation Document all three supported providers (openai, anthropic, ollama) with inline comments, model examples, and placeholder API key. Follows OFBiz convention of committing properties files with placeholder values. --- ai/config/ai.properties | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 ai/config/ai.properties diff --git a/ai/config/ai.properties b/ai/config/ai.properties new file mode 100644 index 000000000..bcdfb2598 --- /dev/null +++ b/ai/config/ai.properties @@ -0,0 +1,38 @@ +# AI Plugin configuration +# This file is gitignored — never commit API keys. +# +# Supported providers: openai | anthropic | ollama +# To switch provider: uncomment the desired block, comment out the others. + +# ------------------------------------------------- +# Provider: openai +# Models: gpt-4o, gpt-4o-mini, gpt-4-turbo, o1-mini +# Docs: https://platform.openai.com/docs/models +# ------------------------------------------------- +ai.provider=openai +ai.model=gpt-4o-mini +ai.apiKey=REPLACE_WITH_YOUR_API_KEY +ai.baseUrl= +ai.timeout=60 + +# ------------------------------------------------- +# Provider: anthropic +# Models: claude-opus-4-5, claude-sonnet-4-5, claude-3-5-haiku-20241022 +# Docs: https://docs.anthropic.com/en/docs/about-claude/models +# ------------------------------------------------- +#ai.provider=anthropic +#ai.model=claude-3-5-haiku-20241022 +#ai.apiKey=REPLACE_WITH_YOUR_API_KEY +#ai.baseUrl= +#ai.timeout=60 + +# ------------------------------------------------- +# Provider: ollama (local, no API key required) +# Models: llama3.2, llama3.1, mistral, gemma3 +# Docs: https://ollama.com/library +# ------------------------------------------------- +#ai.provider=ollama +#ai.model=llama3.2 +#ai.apiKey= +#ai.baseUrl=http://localhost:11434 +#ai.timeout=120