Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
26c8c61
feat(dotAI): replace OpenAIClient with LangChain4J abstraction layer …
ihoffmann-dot Mar 24, 2026
48f268b
feat(dotAI): remove legacy config support, require providerConfig
ihoffmann-dot Mar 24, 2026
0e56d8c
test(dotAI): add unit and integration tests for LangChain4J client layer
ihoffmann-dot Mar 25, 2026
5179db3
fix(dotAI): change BOOL+hidden params to STRING in dotAI.yml
ihoffmann-dot Mar 25, 2026
86057b1
fix(dotAI): remove legacy hidden params from dotAI.yml
ihoffmann-dot Mar 25, 2026
3a5387c
fix(dotAI): update dotAI.yml description to reflect LangChain4J integ…
ihoffmann-dot Mar 26, 2026
ebbeaf1
fix(dotAI): remove legacy OpenAI model validation from AIAppValidator
ihoffmann-dot Mar 26, 2026
3de8a67
fix(dotAI): update /completions/config to reflect providerConfig-base…
ihoffmann-dot Mar 26, 2026
cfdd3cf
fix(dotAI): support maxCompletionTokens for o-series OpenAI models
ihoffmann-dot Mar 27, 2026
bfa54d1
fix(dotAI): replace legacy getApiKey guard with isEnabled in ImageRes…
ihoffmann-dot Mar 30, 2026
6a1213d
refactor(dotAI): remove dead OpenAI model-fetch flow from AIModels
ihoffmann-dot Mar 30, 2026
80820a6
fix(dotAI): handle base64 image responses for models that don't retur…
ihoffmann-dot Mar 30, 2026
07f7bc1
fix(dotAI): send text content (not token IDs) to LangChain4J embeddin…
ihoffmann-dot Mar 30, 2026
d79b37c
fix(dotAI): skip token encoding guard when model not in jtokkit registry
ihoffmann-dot Mar 30, 2026
46028c2
fix(dotAI): add missing IPUtils import in AIModelsTest
ihoffmann-dot Mar 31, 2026
abe115e
refactor(dotAI): PR review comments fixes
ihoffmann-dot Apr 1, 2026
e01cb46
refactor(dotAI): extract build helper in LangChain4jModelFactory to r…
ihoffmann-dot Apr 1, 2026
d9078de
refactor(dotAI): remove unused loadModels, activateModels and getAvai…
ihoffmann-dot Apr 1, 2026
ba7f173
refactor(dotAI): convert ProviderConfig to Immutables interface
ihoffmann-dot Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<graalvm.polyglot.version>25.0.1</graalvm.polyglot.version>
<micrometer.version>1.13.10</micrometer.version>
<opensearch.version>3.3.0</opensearch.version>
<langchain4j.version>1.0.0</langchain4j.version>
</properties>
<dependencyManagement>

Expand Down Expand Up @@ -70,6 +71,14 @@
<scope>import</scope>
</dependency>

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>


<!-- Asynchronous NIO client server framework for the jvm -->
<!-- <dependency>
Expand Down
5 changes: 5 additions & 0 deletions dotCMS/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,11 @@
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
</dependency>
<dependency>
<!-- LangChain4J OpenAI provider: Chat, Embedding, Image models via OpenAI API -->
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
Expand Down
1 change: 1 addition & 0 deletions dotCMS/src/main/java/com/dotcms/ai/AiKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class AiKeys {
public static final String COUNT = "count";
public static final String INPUT = "input";
public static final String RESPONSE_FORMAT = "response_format";
public static final String B64_JSON = "b64_json";

private AiKeys() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public class AsyncEmbeddingsCallStrategy implements EmbeddingsCallStrategy {

@Override
public void bulkEmbed(final List<String> inodes, final EmbeddingsForm embeddingsForm) {
DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new BulkEmbeddingsRunner(inodes, embeddingsForm));
DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new BulkEmbeddingsRunner(inodes, embeddingsForm));
}

@Override
public void embed(final EmbeddingsAPIImpl embeddingsAPI,
final Contentlet contentlet,
final String content,
final String indexName) {
DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(embeddingsAPI, contentlet, content, indexName));
DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(embeddingsAPI, contentlet, content, indexName));
}

}
2 changes: 1 addition & 1 deletion dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
public interface EmbeddingsAPI {

String OPEN_AI_THREAD_POOL_KEY = "OpenAIThreadPool";
String AI_THREAD_POOL_KEY = "AIThreadPool";

void shutdown();

Expand Down
23 changes: 13 additions & 10 deletions dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public int deleteByQuery(@NotNull final String deleteQuery, final Optional<Strin
@Override
public void shutdown() {

Try.run(()->DotConcurrentFactory.getInstance().shutdown(OPEN_AI_THREAD_POOL_KEY));
Try.run(()->DotConcurrentFactory.getInstance().shutdown(AI_THREAD_POOL_KEY));
}

@Override
Expand Down Expand Up @@ -196,7 +196,7 @@ public boolean generateEmbeddingsForContent(@NotNull final Contentlet contentlet
return false;
}

DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(this, contentlet, parsed.get(), indexName));
DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(this, contentlet, parsed.get(), indexName));

return true;
}
Expand Down Expand Up @@ -343,9 +343,11 @@ public Tuple2<Integer, List<Float>> pullOrGenerateEmbeddings(final String conten
.getEncoding()
.map(encoding -> encoding.encode(content))
.orElse(List.of());
final int tokenCount = tokens.isEmpty() ? content.split("\\s+").length : tokens.size();
if (tokens.isEmpty()) {
config.debugLogger(this.getClass(), () -> String.format("No tokens for content ID '%s' were encoded: %s", contentId, content));
return Tuple.of(0, List.of());
config.debugLogger(this.getClass(), () -> String.format(
"Encoding unavailable for content ID '%s', using word count (%d) as token estimate",
contentId, tokenCount));
}

final Tuple3<String, Integer, List<Float>> dbEmbeddings =
Expand All @@ -359,8 +361,8 @@ public Tuple2<Integer, List<Float>> pullOrGenerateEmbeddings(final String conten
}

final Tuple2<Integer, List<Float>> openAiEmbeddings = Tuple.of(
tokens.size(),
sendTokensToOpenAI(contentId, tokens, userId));
tokenCount,
generateEmbeddings(contentId, tokens, content, userId));
saveEmbeddingsForCache(content, openAiEmbeddings);
EMBEDDING_CACHE.put(hashed, openAiEmbeddings);

Expand Down Expand Up @@ -434,20 +436,21 @@ private void saveEmbeddingsForCache(final String content, final Tuple2<Integer,
*
* @return A {@link List} of {@link Float} values representing the embeddings.
*/
private List<Float> sendTokensToOpenAI(final String contentId,
private List<Float> generateEmbeddings(final String contentId,
@NotNull final List<Integer> tokens,
@NotNull final String content,
final String userId) {
final JSONObject json = new JSONObject();
json.put(AiKeys.MODEL, config.getEmbeddingsModel().getCurrentModel());
json.put(AiKeys.INPUT, tokens);
json.put(AiKeys.INPUT, content);
config.debugLogger(this.getClass(), () -> String.format("Content tokens for content ID '%s': %s", contentId, tokens));
final String responseString = AIProxyClient.get()
.callToAI(JSONObjectAIRequest.quickEmbeddings(config, json, userId))
.getResponse();
config.debugLogger(this.getClass(), () -> String.format("OpenAI Response for content ID '%s': %s",
config.debugLogger(this.getClass(), () -> String.format("AI Response for content ID '%s': %s",
contentId, responseString.replace("\n", BLANK)));
final JSONObject jsonResponse = Try.of(() -> new JSONObject(responseString)).getOrElseThrow(e -> {
Logger.error(this, "OpenAI Response String is not a valid JSON", e);
Logger.error(this, "AI Response String is not a valid JSON", e);
config.debugLogger(this.getClass(), () -> String.format("Invalid JSON Response: %s", responseString));
return new DotCorruptedDataException(e);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/
public interface EmbeddingsCallStrategy {

String OPEN_AI_THREAD_POOL_KEY = "OpenAIThreadPool";
String AI_THREAD_POOL_KEY = "AIThreadPool";
/**
* Embeds contentlets based on the provided inodes and form data.
*
Expand Down
26 changes: 19 additions & 7 deletions dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@
import io.vavr.control.Try;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;

public class OpenAIImageAPIImpl implements ImageAPI {
Expand Down Expand Up @@ -99,21 +102,30 @@ public JSONObject sendTextPrompt(final String textPrompt) {
}

private JSONObject createTempFile(final JSONObject imageResponse) {
final String url = imageResponse.optString(AiKeys.URL);
if (UtilMethods.isEmpty(() -> url)) {
Logger.warn(this.getClass(), "imageResponse does not include URL:" + imageResponse);
throw new DotRuntimeException("Image Response does not include URL:" + imageResponse);
}

try {
final String fileName = generateFileName(imageResponse.getString(AiKeys.ORIGINAL_PROMPT));
imageResponse.put("tempFileName", fileName);

final DotTempFile file = tempFileApi.createTempFileFromUrl(fileName, getRequest(), new URL(url), 20);
final String url = imageResponse.optString(AiKeys.URL);
final String b64 = imageResponse.optString(AiKeys.B64_JSON);
final DotTempFile file;

if (!UtilMethods.isEmpty(() -> url)) {
file = tempFileApi.createTempFileFromUrl(fileName, getRequest(), URI.create(url).toURL(), 20);
} else if (!UtilMethods.isEmpty(() -> b64)) {
final byte[] imageBytes = Base64.getDecoder().decode(b64);
file = tempFileApi.createTempFile(fileName, getRequest(), new ByteArrayInputStream(imageBytes));
} else {
Logger.warn(this.getClass(), "imageResponse does not include URL or base64 data:" + imageResponse);
throw new DotRuntimeException("Image Response does not include URL or base64 data:" + imageResponse);
}

imageResponse.put(AiKeys.RESPONSE, file.id);
imageResponse.put("tempFile", file.file.getAbsolutePath());

return imageResponse;
} catch (DotRuntimeException e) {
throw e;
} catch (Exception e) {
imageResponse.put(AiKeys.RESPONSE, e.getMessage());
imageResponse.put(AiKeys.ERROR, e.getMessage());
Expand Down
Loading
Loading