diff --git a/ai-assistant/cms/cms_de.yaml b/ai-assistant/cms/cms_de.yaml index 62f8fd8..0c4da61 100644 --- a/ai-assistant/cms/cms_de.yaml +++ b/ai-assistant/cms/cms_de.yaml @@ -18,7 +18,8 @@ Dialogs: ToggleNonStartableAiFunction: Nicht startbare KI-Funktionen anzeigen ChatDashboard: AiManagement: AI-Management - ClearConversationHistory: Gesprächsverlauf löschen + ClearConversationHistory: Verlauf löschen + ExportHistory: Verlauf exportieren Title: Assistent Dashboard helper: UploadPortalDocument: diff --git a/ai-assistant/cms/cms_en.yaml b/ai-assistant/cms/cms_en.yaml index d5007d8..8ded2ac 100644 --- a/ai-assistant/cms/cms_en.yaml +++ b/ai-assistant/cms/cms_en.yaml @@ -18,7 +18,8 @@ Dialogs: ToggleNonStartableAiFunction: Show non-startable AI functions ChatDashboard: AiManagement: AI Management - ClearConversationHistory: Clear conversation history + ClearConversationHistory: Clear history + ExportHistory: Export history Title: Assistant Dashboard helper: UploadPortalDocument: diff --git a/ai-assistant/cms/cms_es.yaml b/ai-assistant/cms/cms_es.yaml index 351653d..89e3cc4 100644 --- a/ai-assistant/cms/cms_es.yaml +++ b/ai-assistant/cms/cms_es.yaml @@ -18,7 +18,14 @@ Dialogs: ToggleNonStartableAiFunction: Mostrar funciones de IA no iniciables ChatDashboard: AiManagement: Gestión de la IA - ClearConversationHistory: Borrar el historial de conversaciones + ClearConversationHistory: Borrar historial + ExportHistory: |+ + Exportar historial + + + + + Title: Cuadro de mandos auxiliar helper: UploadPortalDocument: diff --git a/ai-assistant/cms/cms_fr.yaml b/ai-assistant/cms/cms_fr.yaml index 2999d78..b100aa1 100644 --- a/ai-assistant/cms/cms_fr.yaml +++ b/ai-assistant/cms/cms_fr.yaml @@ -18,7 +18,9 @@ Dialogs: ToggleNonStartableAiFunction: Afficher les fonctions d'IA non démarrables ChatDashboard: AiManagement: Gestion de l'IA - ClearConversationHistory: Effacer l'historique de conversation + ClearConversationHistory: | + Effacer l'historique + ExportHistory: Exporter l'historique Title: Tableau de bord de l'assistant helper: UploadPortalDocument: diff --git a/ai-assistant/config/variables/AiAssistant/AiFunctions.json b/ai-assistant/config/variables/AiAssistant/AiFunctions.json index 1475c27..6f0c419 100644 --- a/ai-assistant/config/variables/AiAssistant/AiFunctions.json +++ b/ai-assistant/config/variables/AiAssistant/AiFunctions.json @@ -21,23 +21,31 @@ }, { "action": 2, - "case": "User want to find, list out, show, or look up for process" + "case": "Latest message of user is a request to find, list out, show, or look up for process" + }, + { + "action": 2, + "case": "Latest message of user is a normal sentence about his conditions, for example: got sick, pregnant,..." + }, + { + "action": 2, + "case": "Latest message of user is a normal sentence about his changes, for example: buy new house, just married,..." }, { "action": 3, - "case": "User has a question related to process" + "case": "Latest message of user is a question process. Question could be end with question mark or not" }, { "action": 3, - "case": "User has a question related to process widget" + "case": "Latest message of user is a question process widget. Question could be end with question mark or not" }, { "action": 3, - "case": "User has a question related to process list" + "case": "Latest message of user is a question process list. Question could be end with question mark or not" }, { "action": 3, - "case": "User want to do something different from above conditions" + "case": "Latest message of user about something different from above conditions" } ] }, @@ -90,15 +98,15 @@ }, { "action": 3, - "case": "User has a question related to task" + "case": "Latest message of user is a question task. Question could be end with question mark or not" }, { "action": 3, - "case": "User has a question related to task widget" + "case": "Latest message of user is a question task widget. Question could be end with question mark or not" }, { "action": 3, - "case": "User has a question related to task list" + "case": "Latest message of user is a question task list. Question could be end with question mark or not" }, { "action": 3, @@ -151,15 +159,15 @@ }, { "action": 2, - "case": "User has a question related to case" + "case": "Latest message of user is a question case. Question could be end with question mark or not" }, { "action": 2, - "case": "User has a question related to case widget" + "case": "Latest message of user is a question case widget. Question could be end with question mark or not" }, { "action": 2, - "case": "User has a question related to case list" + "case": "Latest message of user is a question case list. Question could be end with question mark or not" }, { "action": 2, @@ -196,11 +204,33 @@ "steps": [ { "stepNo": 0, + "type": "SWITCH", + "cases": [ + { + "action": 5, + "case": "Latest message of user is a question about why the find case function didn't work" + }, + { + "action": 5, + "case": "Latest message of user is a question about what he should do if he cannot find a case" + }, + { + "action": 1, + "case": "User want to do something different from above conditions" + }, + { + "action": 1, + "case": "User want to find case" + } + ] + }, + { + "stepNo": 1, "type": "RE_PHRASE", "useConversationMemory": true, "toolId": "find-cases", - "onRephrase": 1, - "onSuccess": 1, + "onRephrase": 2, + "onSuccess": 2, "saveToHistory": false, "examples": [ { @@ -214,58 +244,32 @@ ] }, { - "stepNo": 1, + "stepNo": 2, "type": "IVY_TOOL", "toolId": "find-cases", - "onSuccess": -1, - "onError": 4 - }, - { - "stepNo": 2, - "type": "TEXT", - "useAI": true, - "customInstruction": "Generate a question similar to 'I have rephrased your request to find case as follows. Could you please confirm if it is correct?'", - "showResultOfStep": 0, - "onSuccess": 3 + "onSuccess": 4, + "onError": 3, + "saveToHistory": false }, { "stepNo": 3, - "type": "SWITCH", - "cases": [ - { - "action": 1, - "case": "User agree" - }, - { - "action": 0, - "case": "User don't agree and refine the condition to find case" - }, - { - "action": -1, - "case": "User don't want to find case anymore" - } - ] + "type": "TEXT", + "text": "Sorry, I cannot find any case matched your request.", + "onSuccess": -1 }, { "stepNo": 4, "type": "TEXT", - "useAI": true, - "customInstruction": "Generate a question similar to 'Sorry, I cannot find any cases matched your request. Could you please provide more details?'", - "onSuccess": 5 + "text": "I found cases matched your request.", + "onSuccess": -1, + "showResultOfStep": 2 }, { "stepNo": 5, - "type": "SWITCH", - "cases": [ - { - "action": 1, - "case": "User clarify or make another request to find case" - }, - { - "action": -1, - "case": "User say he want to cancel or he make another request that irrelevant to the find case function" - } - ] + "type": "KNOWLEDGE_BASE", + "toolId": "portal-support", + "onSuccess": -1, + "onError": -1 } ] }, @@ -282,11 +286,33 @@ "steps": [ { "stepNo": 0, + "type": "SWITCH", + "cases": [ + { + "action": 5, + "case": "Latest message of user is a question about why the find task function didn't work" + }, + { + "action": 5, + "case": "Latest message of user is a question about what he should do if he cannot find a task" + }, + { + "action": 1, + "case": "User want to do something different from above conditions" + }, + { + "action": 1, + "case": "User want to find task" + } + ] + }, + { + "stepNo": 1, "type": "RE_PHRASE", "useConversationMemory": false, "toolId": "find-tasks", - "onRephrase": 1, - "onSuccess": 1, + "onRephrase": 2, + "onSuccess": 2, "saveToHistory": false, "examples": [ { @@ -297,87 +323,59 @@ "before": "find top priority task", "after": "find task has high priority" }, + { + "before": "find working tasks", + "after": "find task has state 'in_progress','open'" + }, + { + "before": "find finished tasks", + "after": "find task has state 'done'" + }, { "before": "find my running task", - "after": "find task has state 'in_progress'" + "after": "find task has state 'in_progress' and the responsible is " }, { - "before": "please find tasks I need to finish this week", - "after": "find task has state 'in_progress','open', expiry date from to , and the responsible is " + "before": "please find tasks I need to finish", + "after": "find task has state 'in_progress','open', and the responsible is " }, { - "before": "List me all the tasks that need completion within the current week", + "before": "List me all the tasks that need completion this week", "after": "find task has state 'in_progress','open', expiry date from to " + }, + { + "before": "List me all the tasks that need completion this month", + "after": "find task has state 'in_progress','open', expiry date from to " } ] }, { - "stepNo": 1, + "stepNo": 2, "type": "IVY_TOOL", "toolId": "find-tasks", - "onSuccess": 6, - "onError": 4, + "onSuccess": 4, + "onError": 3, "saveToHistory": false }, - { - "stepNo": 2, - "type": "TEXT", - "text": "I have rephrased your request as follows. Could you please confirm if it is correct?", - "showResultOfStep": 0, - "onSuccess": 3 - }, { "stepNo": 3, - "type": "SWITCH", - "cases": [ - { - "action": 1, - "case": "User agree" - }, - { - "action": 0, - "case": "User don't agree and suggested another condition to find task" - }, - { - "action": -1, - "case": "User don't want to find task anymore" - } - ] + "type": "TEXT", + "text": "Sorry, I cannot find any task matched your request.", + "onSuccess": -1 }, { "stepNo": 4, "type": "TEXT", - "text": "Sorry, I cannot find any task matched your request. Could you please provide more details or clarify your request?", - "onSuccess": 5 + "text": "I found tasks matched your request.", + "onSuccess": -1, + "showResultOfStep": 2 }, { "stepNo": 5, - "type": "SWITCH", - "cases": [ - { - "action": 0, - "case": "User update the condition to find task" - }, - { - "action": 0, - "case": "User make another request to find task" - }, - { - "action": -1, - "case": "User say he want to cancel or he make another request that irrelevant to the find task function" - }, - { - "action": -1, - "case": "User say good or thank you or seem that he don't want to find task anymore." - } - ] - }, - { - "stepNo": 6, - "type": "TEXT", - "text": "I found tasks matched your request.", + "type": "KNOWLEDGE_BASE", + "toolId": "portal-support", "onSuccess": -1, - "showResultOfStep": 1 + "onError": -1 } ] }, @@ -541,17 +539,39 @@ "steps": [ { "stepNo": 0, + "type": "SWITCH", + "cases": [ + { + "action": 7, + "case": "Latest message of user is a question about why the find process function didn't work" + }, + { + "action": 7, + "case": "Latest message of user is a question about what he should do if he cannot find a process" + }, + { + "action": 1, + "case": "User want to do something different from above conditions" + }, + { + "action": 1, + "case": "User want to find process" + } + ] + }, + { + "stepNo": 1, "type": "IVY_TOOL", "toolId": "find-processes", "onSuccess": -1, - "onError": 1 + "onError": 2 }, { - "stepNo": 1, + "stepNo": 2, "type": "RE_PHRASE", "toolId": "find-processes", - "onRephrase": 2, - "onSuccess": 0, + "onRephrase": 3, + "onSuccess": 1, "examples": [ { "before": "find leave request process", @@ -572,18 +592,18 @@ ] }, { - "stepNo": 2, + "stepNo": 3, "type": "TEXT", "text": "I have rephrased your request as follows. Could you please confirm if it is correct?", - "showResultOfStep": 1, - "onSuccess": 3 + "showResultOfStep": 2, + "onSuccess": 4 }, { - "stepNo": 3, + "stepNo": 4, "type": "SWITCH", "cases": [ { - "action": 4, + "action": 5, "case": "User agree" }, { @@ -591,7 +611,7 @@ "case": "User don't agree" }, { - "action": 0, + "action": 1, "case": "User suggest other processes" }, { @@ -601,17 +621,24 @@ ] }, { - "stepNo": 4, + "stepNo": 5, "type": "IVY_TOOL", "toolId": "find-processes", "onSuccess": -1, - "onError": 5 + "onError": 6 }, { - "stepNo": 5, + "stepNo": 6, "type": "TEXT", "text": "Sorry I cannot find any process matched your request", "onSuccess": -1 + }, + { + "stepNo": 7, + "type": "KNOWLEDGE_BASE", + "toolId": "portal-support", + "onSuccess": -1, + "onError": -1 } ] }, @@ -638,6 +665,14 @@ { "action": 26, "case": "In the chat history, AI didn't metion any process" + }, + { + "action": 30, + "case": "Latest message of user is a question about why the start process function didn't work" + }, + { + "action": 30, + "case": "Latest message of user is a question about what he should do if he cannot start a process" } ] }, @@ -987,6 +1022,13 @@ "case": "User has other request" } ] + }, + { + "stepNo": 30, + "type": "KNOWLEDGE_BASE", + "toolId": "portal-support", + "onSuccess": -1, + "onError": -1 } ] }, diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/bean/AssistantBean.java b/ai-assistant/src/com/axonivy/utils/aiassistant/bean/AssistantBean.java index 1064f7c..4769108 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/bean/AssistantBean.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/bean/AssistantBean.java @@ -2,15 +2,20 @@ import static com.axonivy.utils.aiassistant.enums.SessionAttribute.SELECTED_ASSISTANT_ID; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.List; import javax.annotation.PostConstruct; import javax.faces.bean.ManagedBean; import javax.faces.bean.ViewScoped; +import javax.ws.rs.core.MediaType; import org.apache.commons.collections4.CollectionUtils; +import org.primefaces.model.DefaultStreamedContent; +import org.primefaces.model.StreamedContent; import com.axonivy.portal.components.persistence.converter.BusinessEntityConverter; import com.axonivy.utils.aiassistant.dto.Assistant; @@ -29,6 +34,8 @@ public class AssistantBean implements Serializable { private static final long serialVersionUID = 1683098437048122830L; + private static final String CONVERSATION_FILE_PATTERN = "conversation_%s.json"; + private Assistant assistant; private String assistantId; private List availableAssistants; @@ -88,6 +95,20 @@ public void clearHistory() throws IOException { ChatMessageManager.loadConversation(assistant.getId(), conversationId); } + public StreamedContent exportHistory() { + Conversation conversation = ChatMessageManager + .loadConversation(assistant.getId(), conversationId); + var inputStream = new ByteArrayInputStream( + BusinessEntityConverter.prettyPrintEntityToJsonValue(conversation) + .getBytes(StandardCharsets.UTF_8)); + return DefaultStreamedContent + .builder() + .stream(() -> inputStream) + .contentType(MediaType.APPLICATION_JSON) + .name(String.format(CONVERSATION_FILE_PATTERN, conversationId)) + .build(); + } + public void navigateToAIManagement() { AiNavigator.navigateToAIManagement(); } diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/bean/UploadPortalDocumentBean.java b/ai-assistant/src/com/axonivy/utils/aiassistant/bean/UploadPortalDocumentBean.java index 4328168..15710e2 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/bean/UploadPortalDocumentBean.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/bean/UploadPortalDocumentBean.java @@ -26,6 +26,7 @@ public class UploadPortalDocumentBean { private static final String PORTAL_USER_GUIDE = "portal-user-guide"; + private static final String PERMISSIONS_DOC = "portal-developer-guide/permissions/index.html"; public boolean handlePortalDocumentUpload(FileUploadEvent event) throws IOException { @@ -45,8 +46,9 @@ public boolean handlePortalDocumentUpload(FileUploadEvent event) while (zipEntry != null) { String fileName = zipEntry.getName(); // Only handle files within "portal-user-guide" folder and ending with ".html" - if (fileName.startsWith(PORTAL_USER_GUIDE + "/") - && fileName.endsWith(".html")) { + if ((fileName.startsWith(PORTAL_USER_GUIDE + "/") + && fileName.endsWith(".html")) + || fileName.contentEquals(PERMISSIONS_DOC)) { // Get data from the XHTML file String fileContent = extractFileContent(buffer, fileStream); diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/core/OpenAIBot.java b/ai-assistant/src/com/axonivy/utils/aiassistant/core/OpenAIBot.java index 39b6005..13f2585 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/core/OpenAIBot.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/core/OpenAIBot.java @@ -166,7 +166,7 @@ public void embed(String collectionName, List textSegments) { List embeddings = new ArrayList<>(); for (TextSegment segment : textSegments) { EmbeddingDocument doc = new EmbeddingDocument(); - doc.setMetadata(segment.metadata().asMap()); + doc.setMetadata(segment.metadata().toMap()); doc.setText(segment.text()); doc.setVector(getEmbeddingModel().embed(segment).content().vector()); embeddings.add(doc); @@ -196,12 +196,7 @@ public String chat(Map variables, String promptTemplate) { @Override public String chat(String message) { try { - Ivy.log().error(message); - - String x = getModel().generate(message); - Ivy.log().error(x); - - return x; + return getModel().generate(message); } catch (Exception e) { OpenAIErrorResponse error = BusinessEntityConverter.jsonValueToEntity( e.getCause().getMessage(), OpenAIErrorResponse.class); @@ -213,8 +208,6 @@ public String chat(String message) { public String streamChat(Map variables, String promptTemplate, StreamingResponseHandler handler) { try { - Ivy.log() - .error(PromptTemplate.from(promptTemplate).apply(variables).text()); getChatModel().generate( PromptTemplate.from(promptTemplate).apply(variables).text(), handler); @@ -233,10 +226,14 @@ public String retrieveDocumentsAsString(String collectionName, String query) { Embedding queryEmbedding = embeddingModel.embed(query).content(); List> relevant = toEmbeddingMatch( - getEmbeddingStore().findRelevantDocuments(queryEmbedding, 10, 0.6)); + getEmbeddingStore().findRelevantDocuments(queryEmbedding, 10, 0.7)); relevant.sort(Comparator.comparing(EmbeddingMatch::score)); - return RagPromptTemplates.formatRetrievedDocuments(relevant); + + String formattedRetrievedDocuments = RagPromptTemplates + .formatRetrievedDocuments(relevant); + + return formattedRetrievedDocuments; } @Override diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/EmbeddingDocument.java b/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/EmbeddingDocument.java index d5a842a..ad3b12a 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/EmbeddingDocument.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/EmbeddingDocument.java @@ -5,7 +5,7 @@ public class EmbeddingDocument { private float[] vector; private String text; - private Map metadata; + private Map metadata; public float[] getVector() { return vector; @@ -23,11 +23,11 @@ public void setText(String text) { this.text = text; } - public Map getMetadata() { + public Map getMetadata() { return metadata; } - public void setMetadata(Map metadata) { + public void setMetadata(Map metadata) { this.metadata = metadata; } } \ No newline at end of file diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/IvyOpenSearchEmbeddingStore.java b/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/IvyOpenSearchEmbeddingStore.java index e6f8272..96de313 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/IvyOpenSearchEmbeddingStore.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/core/embedding/IvyOpenSearchEmbeddingStore.java @@ -23,6 +23,7 @@ import org.opensearch.client.opensearch._types.FieldValue; import org.opensearch.client.opensearch._types.InlineScript; import org.opensearch.client.opensearch._types.Time; +import org.opensearch.client.opensearch._types.mapping.KnnVectorMethod; import org.opensearch.client.opensearch._types.mapping.Property; import org.opensearch.client.opensearch._types.mapping.TextProperty; import org.opensearch.client.opensearch._types.mapping.TypeMapping; @@ -66,6 +67,13 @@ public class IvyOpenSearchEmbeddingStore { private static final String INDEX_NOT_EXIST_ERROR = "Cannot find vector store [%s]"; private static final String CANNOT_CONNECT_TO_OPEN_SEARCH = "Cannot connect to the OpenSearch server with URL %s"; + // Possible values: 'faiss' 'lucene', 'nmslib' + // Use Meta's FAISS by default + private static final String SEARCH_ENGINE = "faiss"; + + // Possible values: 'hnsw', 'ivf' (only for FAISS) + private static final String SEARCH_METHOD = "hnsw"; + /** * Creates an instance of OpenSearchEmbeddingStore to connect with * OpenSearch clusters running locally and network reachable. @@ -281,16 +289,25 @@ public void createIndexIfNotExist(int dimension) throws IOException { BooleanResponse response; response = client.indices().exists(c -> c.index(indexName)); if (!response.value()) { - client.indices().create(c -> c.index(indexName).settings(s -> s.knn(true)) + client.indices() + .create(c -> c.index(indexName) + .settings(s -> s.knn(true).knnAlgoParamEfSearch(100)) .mappings(getDefaultMappings(dimension))); } } private TypeMapping getDefaultMappings(int dimension) { + + KnnVectorMethod.Builder builder = new KnnVectorMethod.Builder(); + builder.engine(SEARCH_ENGINE); + builder.name(SEARCH_METHOD); + Map properties = new HashMap<>(4); properties.put("text", Property.of(p -> p.text(TextProperty.of(t -> t)))); properties.put("vector", - Property.of(p -> p.knnVector(k -> k.dimension(dimension)))); + Property + .of(p -> p.knnVector( + k -> k.dimension(dimension).method(builder.build())))); return TypeMapping.of(c -> c.properties(properties)); } diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/AiFlowPromptTemplates.java b/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/AiFlowPromptTemplates.java index b42d626..1161d39 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/AiFlowPromptTemplates.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/AiFlowPromptTemplates.java @@ -12,9 +12,8 @@ public class AiFlowPromptTemplates { Instruction: 1. Read the chat history carefully. The last message of "User" is the request. - 2. Choose the right condition. - 3. ONLY show the value of the "action" field from the selected condition as a tag <>. - Example: If the correct action is "2", then you should show "<2>" + 2. Analyze the request and all conditions then choose the right condition. + 3. Put the value of the "action" field from the selected condition as a tag <>. Example <2> """; public static final String RE_PHRASE_STEP = """ diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/RagPromptTemplates.java b/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/RagPromptTemplates.java index fe0b304..7560c76 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/RagPromptTemplates.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/prompts/RagPromptTemplates.java @@ -34,9 +34,11 @@ public class RagPromptTemplates { ************************* Instruction: - Don't try to create the answer from your own knowledge + - If the contexts are not related to the query, just say you don't know - Restructure the answer to make it easier to understand - Prioritize to answer with contexts have higher score - MUST answer in this language regardless the language of user message: {{language}} + - When show image, please also show its explanation - Criticize the answer: + if you found something incorrect, Tell user that you don't know the answer for his question, please ask something else or try to contact provided contact info. + otherwise, show the answer @@ -180,9 +182,9 @@ public static String getStructuredOutputInstruction(int requestType) { return switch (requestType) { case QUESTION_TYPE_WHAT -> QUESTION_TYPE_WHAT_INSTRUCTION; case QUESTION_TYPE_HOW -> QUESTION_TYPE_HOW_INSTRUCTION; - case QUESTION_TYPE_LIST -> QUESTION_TYPE_LIST_INSTRUCTION; - case QUESTION_TYPE_COMPARE -> QUESTION_TYPE_COMPARE_INSTRUCTION; - case QUESTION_TYPE_WHY_CANNOT -> QUESTION_TYPE_WHY_CANNOT_INSTRUCTION; + case QUESTION_TYPE_LIST -> QUESTION_TYPE_LIST_INSTRUCTION; + case QUESTION_TYPE_COMPARE -> QUESTION_TYPE_COMPARE_INSTRUCTION; + case QUESTION_TYPE_WHY_CANNOT -> QUESTION_TYPE_WHY_CANNOT_INSTRUCTION; default -> ""; }; } diff --git a/ai-assistant/src/com/axonivy/utils/aiassistant/service/PortalDocService.java b/ai-assistant/src/com/axonivy/utils/aiassistant/service/PortalDocService.java index 3179970..907be00 100644 --- a/ai-assistant/src/com/axonivy/utils/aiassistant/service/PortalDocService.java +++ b/ai-assistant/src/com/axonivy/utils/aiassistant/service/PortalDocService.java @@ -1,7 +1,6 @@ package com.axonivy.utils.aiassistant.service; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -22,20 +21,17 @@ import dev.langchain4j.data.segment.TextSegment; public class PortalDocService { - private static final String IVY_DOC_HOST = "https://market.axonivy.com/market-cache/portal/portal-guide/11.3.1/doc/"; - public static final String USER_GUIDE_DIR = "portal-user-guide"; - public static final String DEVELOPER_GUIDE_DIR = "portal-developer-guide"; - public static final String PORTAL_COMPONENT_GUIDE_DIR = "portal-components"; + private static final String IVY_DOC_HOST = "https://market.axonivy.com/market-cache/portal/portal-guide/12.0.0-m266/doc/"; private static final String HEADER_PREFIX = "# "; private static final String SUB_HEADER_PREFIX = "## "; private static final String SMALL_SUB_HEADER_PREFIX = "#### "; + private static final String LIST_ITEM_PREFIX = "- "; private static final String TWO_LEVELS_PATH_PREFIX = "../../"; private static final String LINK_FORMAT = "[%s](%s)"; private static final String IMAGE_FORMAT = "!" + LINK_FORMAT; private static final String CODE_BLOCK = "```"; - private static final String UNKNOWN_CHARACTER_CODE = "?"; public static void createTextIndex(AbstractAIBot bot, String index, List contents) throws IOException { @@ -109,7 +105,7 @@ private static void handleBlockText(String headerKeyword, String keyword, if (CollectionUtils.isNotEmpty(blockText)) { Metadata meta = Metadata.from("keywords", String.join(" ", Arrays.asList(headerKeyword, keyword))); - String keywords = String.format("The main topic of this context: %s, %s", + String keywords = String.format("Keywords: %s, %s", headerKeyword, keyword).concat(System.lineSeparator()); List blockTextWithHeader = new ArrayList<>(); @@ -193,10 +189,6 @@ public static String convertPortalDocument(String raw) { if (StringUtils.isBlank(alt)) { alt = Optional.ofNullable(anchorTag.select("span.std-ref")) .map(Elements::first).map(Element::text).orElse(""); - byte[] ptext = alt.getBytes(StandardCharsets.ISO_8859_1); - alt = new String(ptext, StandardCharsets.UTF_8); - // remove the key icon - alt.replace(UNKNOWN_CHARACTER_CODE, ""); } anchorTag.replaceWith(new TextNode( String.format(LINK_FORMAT, alt, hrefLink))); @@ -223,6 +215,18 @@ public static String convertPortalDocument(String raw) { codeBlockDiv.after(CODE_BLOCK); } + // Replace '\n' inside

tags with a single space + for (Element paragraphTag : document.select("p")) { + String converted = paragraphTag.text().replace(StringUtils.LF, + StringUtils.SPACE); + paragraphTag.replaceWith(new TextNode(converted)); + } + + // Add '- ' before

  • tags + for (Element listTag : document.select("li")) { + listTag.replaceWith(new TextNode(LIST_ITEM_PREFIX + listTag.text())); + } + // Add a new line after each tag for better readability for (Element tag : document.getAllElements()) { if (!Optional.ofNullable(tag).map(Element::parents).isPresent()) { @@ -258,13 +262,13 @@ private static String convertTableToMarkdown(Element table) { Elements headers = table.select("thead tr th"); if (!headers.isEmpty()) { for (Element header : headers) { - markdown.append("| ").append(header.text()).append(" "); + markdown.append("| ").append(header.text()).append(StringUtils.SPACE); } - markdown.append("|\n"); + markdown.append("|").append(StringUtils.LF); // Add separator line for headers headers.forEach(header -> markdown.append("|---")); - markdown.append("|\n"); + markdown.append("|").append(StringUtils.LF); } // Convert table rows @@ -272,9 +276,9 @@ private static String convertTableToMarkdown(Element table) { for (Element row : rows) { Elements cells = row.select("td"); for (Element cell : cells) { - markdown.append("| ").append(cell.text()).append(" "); + markdown.append("| ").append(cell.text()).append(StringUtils.SPACE); } - markdown.append("|\n"); + markdown.append("|").append(StringUtils.LF); } return markdown.toString(); diff --git a/ai-assistant/src_hd/com/axonivy/utils/aiassistant/component/ChatDashboard/ChatDashboard.xhtml b/ai-assistant/src_hd/com/axonivy/utils/aiassistant/component/ChatDashboard/ChatDashboard.xhtml index a79a45a..e68d9a8 100644 --- a/ai-assistant/src_hd/com/axonivy/utils/aiassistant/component/ChatDashboard/ChatDashboard.xhtml +++ b/ai-assistant/src_hd/com/axonivy/utils/aiassistant/component/ChatDashboard/ChatDashboard.xhtml @@ -87,23 +87,32 @@
    - - - - + + + + + + + + +
    - diff --git a/ai-assistant/webContent/resources/css/chatbot.css b/ai-assistant/webContent/resources/css/chatbot.css index 2e9703b..5f04cf5 100644 --- a/ai-assistant/webContent/resources/css/chatbot.css +++ b/ai-assistant/webContent/resources/css/chatbot.css @@ -642,3 +642,20 @@ body.dark .chatbot-panel .chat-message.system-response-expand-button { color: var(--gray-100); } +body .ui-button.ui-widget.icon-more-menu-button { + background: var(--surface-a); + border-color: var(--surface-900); +} + +body .ui-button.ui-widget.icon-more-menu-button { + color: var(--widget-header-icon); +} + +.ui-menu-items, body .ui-menu .ui-menu-list .ui-menuitem:not(:last-child), .action-step-item:not(:last-child) { + border-bottom: solid 1px var(--gray-50); +} + +body .ui-menu .ui-menu-list .ui-menuitem .action-step-item.red .ui-menuitem-text, +body .ui-menu .ui-menu-list .ui-menuitem .action-step-item.red .si { + color: var(--red-500); +} diff --git a/ai-assistant/webContent/resources/js/ai-assistant.js b/ai-assistant/webContent/resources/js/ai-assistant.js index 811d54c..fe1d27c 100644 --- a/ai-assistant/webContent/resources/js/ai-assistant.js +++ b/ai-assistant/webContent/resources/js/ai-assistant.js @@ -580,7 +580,7 @@ function ViewAI(uri) { break; case 'IVY_TOOL': icon = 'si si-lg si-cog-double-2'; - header = 'Processing Ivy tool'; + header = 'Run Ivy tool'; break; case 'RE_PHRASE': icon = 'si si-lg si-messages-bubble-check'; @@ -767,9 +767,18 @@ function ViewAI(uri) { // Update existing streaming message streamingMessage.get(0).innerHTML = cloneTemplate.innerHTML; + // While streaming the response, show text instead of image + const renderer = new marked.Renderer(); + renderer.image = function(text) { + return text; + } + + marked.use({ renderer }); + if (!isIFrame(streamingMessage.get(0).innerHTML)) { streamingMessage.find('.js-message').get(0).innerHTML = marked.parse(streamingText); - streamingMessage.find('.js-message').find('img').addClass('w-full'); + streamingMessage.find('.js-message').find('img').remove(); + streamingMessage.find('.js-message').find('em').addClass('block'); streamingMessage.find('.js-message').find('a').attr('target', '_blank').addClass('underline'); } } @@ -781,6 +790,10 @@ function ViewAI(uri) { return; } + // User default renderer + const renderer = new marked.Renderer(); + marked.use({ renderer }); + let converted = isIFrame(streamingValue) ? convertIFrame(streamingValue) : marked.parse(streamingValue); if (typeof jsMessageList !== 'undefined') { @@ -789,12 +802,14 @@ function ViewAI(uri) { if (streamingMessage.length > 0) { streamingMessage.removeClass('streaming'); $(streamingMessage).find('.js-message').get(0).innerHTML = converted; - $($(streamingMessage).find('.js-message').get(0)).find('img').addClass('w-full'); + $($(streamingMessage).find('.js-message').get(0)).find('img').addClass('max-w-full'); + $($(streamingMessage).find('.js-message').get(0)).find('em').addClass('block'); $($(streamingMessage).find('.js-message').get(0)).find('a').attr('target', '_blank').addClass('underline'); } else { const messages = messageList.find('.chat-message-container').not('.my-message').find('.js-message'); messages.get(messages.length - 1).innerHTML = converted; - $(messages.get(messages.length - 1)).find('img').addClass('w-full'); + $(messages.get(messages.length - 1)).find('img').addClass('max-w-full'); + $(messages.get(messages.length - 1)).find('em').addClass('block'); $(messages.get(messages.length - 1)).find('a').attr('target', '_blank').addClass('underline'); }