Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const EXAMPLE_CONFIG = {
image: {
provider: 'openai',
apiKey: 'sk-...',
model: 'dall-e-3'
model: 'gpt-image-1'
},
settings: {
rolePrompt: 'You are dotCMSbot, an AI assistant to help content creators.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dotcms.ai.rest.forms.EmbeddingsForm;
import com.dotcms.contenttype.model.field.Field;
import com.dotmarketing.beans.Host;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.util.Logger;
Expand Down Expand Up @@ -47,12 +48,16 @@ public void run() {
.stream()
.filter(f -> embeddingsForm.fieldsAsList().contains(f.variable().toLowerCase()))
.collect(Collectors.toList());
final Host host = Try
.of(() -> APILocator.getHostAPI().find(
contentlet.getHost(), APILocator.systemUser(), false))
.getOrElse(APILocator.systemHost());
// if a velocity template is passed in, use it. Otherwise, try the fields
if (!APILocator.getDotAIAPI().getEmbeddingsAPI().generateEmbeddingsForContent(
if (!APILocator.getDotAIAPI().getEmbeddingsAPI(host).generateEmbeddingsForContent(
Comment thread
ihoffmann-dot marked this conversation as resolved.
contentlet,
embeddingsForm.velocityTemplate,
embeddingsForm.indexName)) {
APILocator.getDotAIAPI().getEmbeddingsAPI().generateEmbeddingsForContent(contentlet, fields, embeddingsForm.indexName);
APILocator.getDotAIAPI().getEmbeddingsAPI(host).generateEmbeddingsForContent(contentlet, fields, embeddingsForm.indexName);
}
} catch (Exception e) {
Logger.warn(this.getClass(), "unable to embed content:" + inode + " error:" + e.getMessage(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.chat.ChatModel;
Expand Down Expand Up @@ -379,18 +382,43 @@ static List<ChatMessage> toMessages(final JSONArray messagesArray) {
for (int i = 0; i < messagesArray.length(); i++) {
final JSONObject msg = messagesArray.getJSONObject(i);
final String role = msg.optString(AiKeys.ROLE, AiKeys.USER).toLowerCase();
final String content = msg.optString(AiKeys.CONTENT, "");
final Object contentRaw = msg.opt(AiKeys.CONTENT);
if ("system".equals(role)) {
messages.add(new SystemMessage(content));
messages.add(new SystemMessage(contentRaw != null ? contentRaw.toString() : ""));
} else if ("assistant".equals(role)) {
messages.add(new AiMessage(content));
messages.add(new AiMessage(contentRaw != null ? contentRaw.toString() : ""));
} else if (contentRaw instanceof JSONArray) {
messages.add(toMultimodalUserMessage((JSONArray) contentRaw));
} else {
messages.add(new UserMessage(content));
messages.add(new UserMessage(contentRaw != null ? contentRaw.toString() : ""));
}
}
return messages;
}

static UserMessage toMultimodalUserMessage(final JSONArray contentParts) {
final List<Content> parts = new ArrayList<>();
for (int i = 0; i < contentParts.length(); i++) {
final JSONObject part = contentParts.getJSONObject(i);
parts.add("image_url".equals(part.optString("type", "text"))
? toImageContent(part)
: TextContent.from(part.optString("text", "")));
}
return UserMessage.from(parts);
}

private static Content toImageContent(final JSONObject part) {
final String url = part.optJSONObject("image_url").optString("url", "");
if (url.startsWith("data:")) {
final int semicolon = url.indexOf(';');
final int comma = url.indexOf(',');
if (semicolon > 0 && comma > semicolon) {
return ImageContent.from(url.substring(comma + 1), url.substring(5, semicolon));
}
}
return ImageContent.from(url);
}

static String toChatResponseJson(final ChatResponse response) {
final JSONObject message = new JSONObject();
message.put(AiKeys.ROLE, "assistant");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dotcms.ai.listener;

import com.dotcms.ai.api.EmbeddingsAPI;
import com.dotcms.ai.app.AppConfig;
import com.dotcms.ai.app.ConfigService;
import com.dotcms.ai.db.EmbeddingsDTO;
Expand Down Expand Up @@ -78,12 +79,14 @@ public void onDeleted(final ContentletDeletedEvent<Contentlet> contentletDeleted
deleteFromIndexes(contentlet);
}

private AppConfig getAppConfig(final String hostId) {
final Host host = Try
private Host resolveHost(final String hostId) {
return Try
.of(() -> APILocator.getHostAPI().find(hostId, APILocator.systemUser(), false))
.getOrElse(APILocator.systemHost());
}

final AppConfig appConfig = ConfigService.INSTANCE.config(host);
private AppConfig getAppConfig(final String hostId) {
final AppConfig appConfig = ConfigService.INSTANCE.config(resolveHost(hostId));
if (!appConfig.isEnabled()) {
appConfig.debugLogger(
getClass(),
Expand Down Expand Up @@ -121,17 +124,18 @@ private void addToIndexesIfNeeded(final Contentlet contentlet) {
}

try {
final Host host = resolveHost(contentlet.getHost());
final JSONObject config = getConfigJson(contentlet.getHost());

for (final Entry<String, Object> entry : (Set<Entry<String, Object>>) config.entrySet()) {
final String indexName = entry.getKey();
final EmbeddingsAPI embeddingsAPI = APILocator.getDotAIAPI().getEmbeddingsAPI(host);
final Map<String, List<Field>> typesAndFields =
APILocator.getDotAIAPI().getEmbeddingsAPI().parseTypesAndFields((String) entry.getValue());
embeddingsAPI.parseTypesAndFields((String) entry.getValue());
typesAndFields.entrySet()
.stream()
.filter(typeFields -> contentType.equalsIgnoreCase(typeFields.getKey()))
.forEach(e -> APILocator.getDotAIAPI().getEmbeddingsAPI()
.generateEmbeddingsForContent(
.forEach(e -> embeddingsAPI.generateEmbeddingsForContent(
contentlet,
e.getValue(),
indexName));
Expand All @@ -149,14 +153,15 @@ private void addToIndexesIfNeeded(final Contentlet contentlet) {
@WrapInTransaction
private void deleteFromIndexes(final Contentlet contentlet) {
try {
final Host host = resolveHost(contentlet.getHost());
getConfigJson(contentlet.getHost());

final EmbeddingsDTO dto = new EmbeddingsDTO.Builder()
.withIdentifier(contentlet.getIdentifier())
.withLanguage(contentlet.getLanguageId())
.withIndexName(ALL_INDICES)
.build();
APILocator.getDotAIAPI().getEmbeddingsAPI().deleteEmbedding(dto);
APILocator.getDotAIAPI().getEmbeddingsAPI(host).deleteEmbedding(dto);
} catch (final Exception e) {
Logger.error(getClass(), "Error deleting content from embeddings index: " + e.getMessage(), e);
throw new DotRuntimeException("Error deleting content from embeddings index: " + e.getMessage(), e);
Expand Down
12 changes: 11 additions & 1 deletion dotCMS/src/main/java/com/dotcms/ai/rest/SearchResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,20 @@ public final Response searchByPost(@Context final HttpServletRequest request,
.init()
.getUser();

final Host host = WebAPILocator.getHostWebAPI().getCurrentHostNoThrow(request);
final Map<String, Map<String, Object>> knownIndexes =
APILocator.getDotAIAPI().getEmbeddingsAPI(host).countEmbeddingsByIndex();
if (!knownIndexes.containsKey(form.indexName)) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of(AiKeys.ERROR, "Index '" + form.indexName + "' not found. Known indexes: " + knownIndexes.keySet()))
.type(MediaType.APPLICATION_JSON)
.build();
}

final EmbeddingsDTO searcher = EmbeddingsDTO.from(form).withUser(user).build();

return Response.ok(
APILocator.getDotAIAPI().getEmbeddingsAPI().searchForContent(searcher).toString(),
APILocator.getDotAIAPI().getEmbeddingsAPI(host).searchForContent(searcher).toString(),
MediaType.APPLICATION_JSON).build();
}

Expand Down
14 changes: 4 additions & 10 deletions dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
Content index to search:
</th>
<td>
<select name="indexName" id="indexNameChat" style="min-width:400px;">
<option disabled="true" placeholder="Select an Index">Select an Index</option>
<select name="indexName" id="indexNameChat" required style="min-width:400px;">
<option disabled value="">Select an Index</option>
</select>
</td>
</tr>
Expand Down Expand Up @@ -98,7 +98,7 @@
</th>
<td><span class="clearPromptX" id="searchQueryX" onclick="clearPrompt('searchQuery')"
style="visibility: hidden">&#10006;</span>
<textarea class="prompt" name="prompt" id="searchQuery"
<textarea class="prompt" name="prompt" id="searchQuery" required
onkeyup="showClearPrompt('searchQuery')"
onchange="showClearPrompt('searchQuery')"
placeholder="Search text or phrase"></textarea>
Expand Down Expand Up @@ -361,13 +361,7 @@
Size:
</th>
<td>
<select name="size" style="min-width:400px;">
<option value="1024x1024">1024x1024 (Square)</option>
<option value="1024x1792">1024x1792 (Vertical)</option>
<option value="1792x1024" selected>1792x1024 (Horizontal)</option>


</select>
<input type="text" name="size" value="1024x1024" style="min-width:400px;" readonly>
</td>
</tr>
<tr>
Expand Down
50 changes: 28 additions & 22 deletions dotCMS/src/main/webapp/html/portlet/ext/dotai/dotai.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,28 @@ const writeIndexesToDropdowns = async () => {

const writeModelToDropdown = async () => {
const modelName = document.getElementById("modelName");
let options = modelName.getElementsByTagName('option');
modelName.innerHTML = '<option disabled value="">Select a Model</option>';

for (i = 1; i < options.length; i++) {
indexName.removeChild(options[i]);
let models = [];
try {
const providerConfig = JSON.parse(dotAiState.config.providerConfig);
const chatModel = providerConfig?.chat?.model ?? '';
models = chatModel.split(',').map(m => m.trim()).filter(m => m.length > 0);
} catch (e) {
Comment thread
ihoffmann-dot marked this conversation as resolved.
console.error('[DotAI] writeModelToDropdown: providerConfig missing or not valid JSON', e);
}

for (i = 0; i < dotAiState.config.availableModels.length; i++) {
if (dotAiState.config.availableModels[i].type !== 'TEXT') {
continue;
}

const newOption = document.createElement("option");
newOption.value = dotAiState.config.availableModels[i].name;
newOption.text = `${dotAiState.config.availableModels[i].name}`
if (dotAiState.config.availableModels[i].current) {
newOption.selected = true;
newOption.text = `${dotAiState.config.availableModels[i].name} (default)`
}
modelName.appendChild(newOption);
if (models.length === 0) {
console.warn('[DotAI] writeModelToDropdown: models is empty, dropdown will not be populated');
}

models.forEach((model, index) => {
Comment thread
ihoffmann-dot marked this conversation as resolved.
const opt = document.createElement("option");
opt.value = model;
opt.text = index === 0 ? `${model} (default)` : model;
if (index === 0) opt.selected = true;
modelName.appendChild(opt);
});
};


Expand Down Expand Up @@ -566,27 +568,28 @@ const doImageJsonDebounced = async () => {
const temp = json.response;
const width = formData.size.split("x")[0];
const height = formData.size.split("x")[1];
const jsonString=JSON.stringify(json, 2);
const jsonString = JSON.stringify(json, null, 2);
const rewrittenPrompt = json.revised_prompt;
const imageTemplate =`
<div style="width:100%;max-width:800px;position:relative;text-align:center;border:1px solid silver;padding:1rem;">
<div style="width:100%;max-width:800px;position:relative;text-align:center;border:1px solid silver;padding:1rem;overflow:hidden;">
<a href="/dA/${temp}/asset.png" target="_blank">
<img src="/dA/${temp}/asset.png" style="max-width:750px;max-height:750px;display: block;margin:auto;" />
</a>

<div style="padding:1rem;margin:auto;text-align: center">
<button id="saveImageButton" class="button dijit dijitReset dijitInline dijitButton"
onclick="saveImage('${temp}')">
Save
</button><br>
<div id="imageSavedMessage">&nbsp;</div>
</div>
<div style="border:1px solid silver;padding:1rem;margin:auto;text-align: left">
<div style="border:1px solid silver;padding:1rem;margin:auto;text-align: left;overflow-wrap:break-word;word-break:break-word;">
<b>OpenAI Prompt (Rewritten):</b> <br>
${rewrittenPrompt}
</div>
<div style="border:1px solid silver;padding:1rem;margin:auto;text-align: left">
<b>JSON Response:</b> <br>${jsonString}
<div style="border:1px solid silver;padding:1rem;margin:auto;text-align: left;">
<b>JSON Response:</b>
<pre style="white-space:pre-wrap;word-break:break-word;overflow-x:auto;overflow-y:auto;max-height:300px;margin:0.5rem 0 0 0;font-size:0.85em;">${jsonString}</pre>
</div>
</div>

Expand Down Expand Up @@ -646,6 +649,9 @@ const clearSaveMessage =() =>{


const doSearchChatJson = () => {
if (!document.getElementById("chatForm").reportValidity()) {
return;
}
document.getElementById("submitChat").style.display = "none";
document.getElementById("loaderChat").style.display = "block";
setTimeout(function () {
Expand Down
Loading
Loading