001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.ai; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.contenteditor.I_CmsContentTranslator; 032import org.opencms.configuration.CmsConfigurationException; 033import org.opencms.configuration.CmsParameterConfiguration; 034import org.opencms.file.CmsFile; 035import org.opencms.file.CmsObject; 036import org.opencms.gwt.shared.CmsGwtConstants; 037import org.opencms.i18n.CmsLocaleManager; 038import org.opencms.main.CmsLog; 039import org.opencms.main.OpenCms; 040import org.opencms.xml.content.CmsXmlContent; 041import org.opencms.xml.content.I_CmsXmlContentAugmentation; 042 043import java.util.List; 044import java.util.Locale; 045import java.util.concurrent.atomic.AtomicInteger; 046import java.util.concurrent.atomic.AtomicReference; 047 048import org.apache.commons.logging.Log; 049 050import dev.langchain4j.model.chat.response.ChatResponse; 051import dev.langchain4j.model.chat.response.PartialResponse; 052import dev.langchain4j.model.chat.response.PartialResponseContext; 053import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; 054 055/** 056 * LLM implementation of the I_CmsContentTranslator interface. 057 * 058 * <p>Reads the LLM credentials from a JSON file whose file system path is passed via the 'config' configuration parameter. 059 */ 060public class CmsAiContentTranslation implements I_CmsContentTranslator { 061 062 public class Augmentation implements I_CmsXmlContentAugmentation { 063 064 @Override 065 public void augmentContent(Context context) throws Exception { 066 067 CmsObject cms = context.getCmsObject(); 068 final Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 069 CmsXmlContent content = context.getContent(); 070 Locale sourceLocale = context.getLocale(); 071 String targetLocaleParam = context.getParameter(CmsGwtConstants.PARAM_TARGET_LOCALE); 072 Locale targetLocale = CmsLocaleManager.getLocale(targetLocaleParam); 073 CmsAiProviderConfig config = CmsAiProviderConfig.loadFromSecretStore(); 074 if (config == null) { 075 return; 076 } 077 078 CmsAiTranslator translator = new CmsAiTranslator(cms, config, content); 079 AtomicInteger charsReceived = new AtomicInteger(); 080 AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>(); 081 082 CmsXmlContent result = translator.translateXmlContent( 083 sourceLocale, 084 targetLocale, 085 new StreamingChatResponseHandler() { 086 087 @Override 088 public void onCompleteResponse(ChatResponse completeResponse) { 089 090 LOG.debug("Response received: " + completeResponse.aiMessage().text()); 091 } 092 093 @Override 094 public void onError(Throwable error) { 095 096 LOG.error(error.getLocalizedMessage(), error); 097 errorRef.set(error); 098 } 099 100 @Override 101 public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext resContext) { 102 103 charsReceived.addAndGet(partialResponse.text().length()); 104 if (context.isAborted()) { 105 resContext.streamingHandle().cancel(); 106 } else { 107 String html = CmsTranslationUtil.getWaitMessage(wpLocale); 108 context.progress(html); 109 } 110 } 111 112 }); 113 if (errorRef.get() != null) { 114 throw (Exception)errorRef.get(); 115 } 116 if (result != null) { 117 context.setResult(result); 118 context.setHtmlMessage( 119 buildFeedbackHtml( 120 cms, 121 sourceLocale, 122 targetLocale, 123 translator.getNumSuccessfulFieldUpdates(), 124 translator.getConflictFields())); 125 context.setNextLocale(targetLocale); 126 } else { 127 String nothingTranslated = Messages.get().getBundle(wpLocale).key( 128 Messages.GUI_TRANSLATION_NOTHING_TRANSLATED_0); 129 context.setHtmlMessage("<p>" + nothingTranslated + "</p>"); 130 } 131 } 132 133 /** 134 * Builds the HTML for the feedback screen. 135 * 136 * @param cms the CMS context 137 * @param sourceLocale the translation source locale 138 * @param targetLocale the translation target locale 139 * @param numSuccessfulFieldUpdates the number of translated fields 140 * @param conflictFields the list of fields with conflicts 141 * @return 142 */ 143 private String buildFeedbackHtml( 144 CmsObject cms, 145 Locale sourceLocale, 146 Locale targetLocale, 147 int numSuccessfulFieldUpdates, 148 List<String> conflictFields) { 149 150 StringBuilder buffer = new StringBuilder(); 151 Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 152 buffer.append("<p>"); 153 buffer.append( 154 Messages.get().getBundle(wpLocale).key( 155 Messages.GUI_TRANSLATION_FEEDBACK_3, 156 numSuccessfulFieldUpdates, 157 sourceLocale.getDisplayName(wpLocale), 158 targetLocale.getDisplayName(wpLocale))); 159 buffer.append("</p>"); 160 if (conflictFields.size() > 0) { 161 buffer.append("<p>"); 162 buffer.append(Messages.get().getBundle(wpLocale).key(Messages.GUI_TRANSLATION_FEEDBACK_CONFLICTS_0)); 163 buffer.append("</p>"); 164 buffer.append("<ul>"); 165 for (String field : conflictFields) { 166 buffer.append("<li>"); 167 buffer.append(field); 168 buffer.append("</li>"); 169 } 170 buffer.append("</ul>"); 171 } 172 return buffer.toString(); 173 } 174 } 175 176 /** Parameter from which the config file path is read. */ 177 public static final String PARAM_CONFIG_FILE = "config"; 178 179 /** Logger instance for this class. */ 180 private static final Log LOG = CmsLog.getLog(CmsAiContentTranslation.class); 181 182 /** The configuration parameters. */ 183 private CmsParameterConfiguration m_config = new CmsParameterConfiguration(); 184 185 /** 186 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 187 */ 188 @Override 189 public void addConfigurationParameter(String paramName, String paramValue) { 190 191 m_config.add(paramName, paramValue); 192 193 } 194 195 /** 196 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 197 */ 198 @Override 199 public CmsParameterConfiguration getConfiguration() { 200 201 return m_config; 202 } 203 204 /** 205 * @see org.opencms.ade.contenteditor.I_CmsContentTranslator#getContentAugmentation() 206 */ 207 @Override 208 public I_CmsXmlContentAugmentation getContentAugmentation() { 209 210 return new Augmentation(); 211 } 212 213 /** 214 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 215 */ 216 @Override 217 public void initConfiguration() throws CmsConfigurationException { 218 219 } 220 221 /** 222 * @see org.opencms.ade.contenteditor.I_CmsContentTranslator#initialize(org.opencms.file.CmsObject) 223 */ 224 @Override 225 public void initialize(CmsObject cms) { 226 227 } 228 229 /** 230 * @see org.opencms.ade.contenteditor.I_CmsContentTranslator#isEnabled(org.opencms.file.CmsObject, org.opencms.ade.configuration.CmsADEConfigData, org.opencms.file.CmsFile) 231 */ 232 @Override 233 public boolean isEnabled(CmsObject cms, CmsADEConfigData config, CmsFile file) { 234 235 return null != CmsAiProviderConfig.loadFromSecretStore(); 236 } 237 238}