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}