diff --git a/bot/admin/server/src/main/kotlin/BotAdminService.kt b/bot/admin/server/src/main/kotlin/BotAdminService.kt index 0cd624f092..16792737b0 100644 --- a/bot/admin/server/src/main/kotlin/BotAdminService.kt +++ b/bot/admin/server/src/main/kotlin/BotAdminService.kt @@ -17,6 +17,7 @@ package ai.tock.bot.admin import ai.tock.bot.admin.FaqAdminService.FAQ_CATEGORY +import ai.tock.bot.admin.annotation.* import ai.tock.bot.admin.answer.AnswerConfiguration import ai.tock.bot.admin.answer.AnswerConfigurationType.builtin import ai.tock.bot.admin.answer.AnswerConfigurationType.script @@ -45,6 +46,7 @@ import ai.tock.bot.admin.story.dump.* import ai.tock.bot.admin.user.UserReportDAO import ai.tock.bot.connector.ConnectorType import ai.tock.bot.definition.IntentWithoutNamespace +import ai.tock.bot.engine.action.Action import ai.tock.bot.engine.config.SatisfactionIntent import ai.tock.bot.engine.dialog.Dialog import ai.tock.bot.engine.dialog.DialogFlowDAO @@ -61,6 +63,7 @@ import ai.tock.nlp.front.shared.config.* import ai.tock.nlp.front.shared.config.ClassifiedSentenceStatus.model import ai.tock.nlp.front.shared.config.ClassifiedSentenceStatus.validated import ai.tock.shared.* +import ai.tock.shared.exception.rest.NotFoundException import ai.tock.shared.security.UserLogin import ai.tock.shared.security.key.HasSecretKey import ai.tock.shared.security.key.SecretKey @@ -69,6 +72,7 @@ import ai.tock.translator.* import com.github.salomonbrys.kodein.instance import mu.KotlinLogging import org.litote.kmongo.Id +import org.litote.kmongo.newId import org.litote.kmongo.toId import java.time.Instant import java.util.* @@ -147,6 +151,211 @@ object BotAdminService { } } + fun addCommentToAnnotation( + dialogId: String, + actionId: String, + eventDTO: BotAnnotationEventDTO, + user: String + ): BotAnnotationEvent { + + if (eventDTO.type != BotAnnotationEventType.COMMENT) { + throw IllegalArgumentException("Only COMMENT events are allowed") + } + + require(eventDTO.comment != null) { "Comment is required for COMMENT event type" } + + val annotation = dialogReportDAO.getAnnotationByActionId(dialogId, actionId) + ?: throw IllegalStateException("Annotation not found") + + val event = BotAnnotationEventComment( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + comment = eventDTO.comment!! + ) + + dialogReportDAO.addAnnotationEvent(dialogId, actionId, event) + + return event + } + + fun updateAnnotationEvent( + dialogId: String, + actionId: String, + eventId: String, + eventDTO: BotAnnotationEventDTO, + user: String + ): BotAnnotationEvent { + val existingEvent = dialogReportDAO.getAnnotationEvent(dialogId, actionId, eventId) + ?: throw IllegalArgumentException("Event not found") + + if (existingEvent.type != BotAnnotationEventType.COMMENT) { + throw IllegalArgumentException("Only comment events can be updated") + } + + if (eventDTO.type != BotAnnotationEventType.COMMENT) { + throw IllegalArgumentException("Event type must be COMMENT") + } + + require(eventDTO.comment != null) { "Comment must be provided" } + + val annotation = dialogReportDAO.getAnnotationByActionId(dialogId, actionId) + ?: throw IllegalStateException("Annotation not found") + + val existingCommentEvent = existingEvent as BotAnnotationEventComment + val updatedEvent = existingCommentEvent.copy( + comment = eventDTO.comment!!, + lastUpdateDate = Instant.now() + ) + + dialogReportDAO.updateAnnotationEvent(dialogId, actionId, eventId, updatedEvent) + + return updatedEvent + } + + fun deleteAnnotationEvent( + dialogId: String, + actionId: String, + annotationId: String, + eventId: String, + user: String + ) { + val existingEvent = dialogReportDAO.getAnnotationEvent(dialogId, actionId, eventId) + ?: throw IllegalArgumentException("Event not found") + + if (existingEvent.type != BotAnnotationEventType.COMMENT) { + throw IllegalArgumentException("Only comment events can be deleted") + } + + val annotation = dialogReportDAO.getAnnotationByActionId(dialogId, actionId) + ?: throw IllegalStateException("Annotation not found") + + + dialogReportDAO.deleteAnnotationEvent(dialogId, actionId, eventId) + } + + fun updateAnnotation( + dialogId: String, + actionId: String, + annotationId: String, + updatedAnnotationDTO: BotAnnotationUpdateDTO, + user: String + ): BotAnnotation { + val existingAnnotation = dialogReportDAO.getAnnotation(dialogId, actionId, annotationId) + ?: throw IllegalStateException("Annotation not found") + + val events = mutableListOf() + + updatedAnnotationDTO.state?.let { newState -> + if (existingAnnotation.state != newState) { + events.add( + BotAnnotationEventState( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + before = existingAnnotation.state.name, + after = newState.name + ) + ) + existingAnnotation.state = newState + } + } + + updatedAnnotationDTO.reason?.let { newReason -> + if (existingAnnotation.reason != newReason) { + events.add( + BotAnnotationEventReason( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + before = existingAnnotation.reason?.name, + after = newReason.name + ) + ) + existingAnnotation.reason = newReason + } + } + + updatedAnnotationDTO.groundTruth?.let { newGroundTruth -> + if (existingAnnotation.groundTruth != newGroundTruth) { + events.add( + BotAnnotationEventGroundTruth( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + before = existingAnnotation.groundTruth, + after = newGroundTruth + ) + ) + existingAnnotation.groundTruth = newGroundTruth + } + } + + updatedAnnotationDTO.description?.let { newDescription -> + if (existingAnnotation.description != newDescription) { + events.add( + BotAnnotationEventDescription( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + before = existingAnnotation.description, + after = newDescription + ) + ) + existingAnnotation.description = newDescription + } + } + + existingAnnotation.lastUpdateDate = Instant.now() + + existingAnnotation.events.addAll(events) + + dialogReportDAO.updateAnnotation(dialogId, actionId, existingAnnotation) + + return existingAnnotation + } + + fun createAnnotation( + dialogId: String, + actionId: String, + annotationDTO: BotAnnotationDTO, + user: String + ): BotAnnotation { + + if (dialogReportDAO.annotationExists(dialogId, actionId)) { + throw IllegalStateException("Une annotation existe déjà pour cette action.") + } + + val annotation = BotAnnotation( + state = annotationDTO.state, + reason = annotationDTO.reason, + description = annotationDTO.description, + groundTruth = annotationDTO.groundTruth, + actionId = actionId, + dialogId = dialogId, + events = mutableListOf(), + lastUpdateDate = Instant.now() + ) + + val event = BotAnnotationEventState( + eventId = newId(), + creationDate = Instant.now(), + lastUpdateDate = Instant.now(), + user = user, + before = null, + after = annotationDTO.state.name + ) + + annotation.events.add(event) + dialogReportDAO.updateAnnotation(dialogId, actionId, annotation) + return annotation + } + fun createOrGetIntent( namespace: String, intentName: String, diff --git a/bot/admin/server/src/main/kotlin/BotAdminVerticle.kt b/bot/admin/server/src/main/kotlin/BotAdminVerticle.kt index 00f7e2e058..0bbeec1b9f 100644 --- a/bot/admin/server/src/main/kotlin/BotAdminVerticle.kt +++ b/bot/admin/server/src/main/kotlin/BotAdminVerticle.kt @@ -16,6 +16,7 @@ package ai.tock.bot.admin + import ai.tock.bot.admin.BotAdminService.createI18nRequest import ai.tock.bot.admin.BotAdminService.dialogReportDAO import ai.tock.bot.admin.BotAdminService.getBotConfigurationByApplicationIdAndBotId @@ -30,6 +31,7 @@ import ai.tock.bot.admin.service.* import ai.tock.bot.admin.story.dump.StoryDefinitionConfigurationDumpImport import ai.tock.bot.admin.test.TestPlanService import ai.tock.bot.admin.test.findTestService +import ai.tock.bot.admin.verticle.DialogVerticle import ai.tock.bot.admin.verticle.IndicatorVerticle import ai.tock.bot.connector.ConnectorType.Companion.rest import ai.tock.bot.connector.ConnectorTypeConfiguration @@ -39,9 +41,7 @@ import ai.tock.bot.engine.config.SATISFACTION_MODULE_ID import ai.tock.bot.engine.config.UploadedFilesService import ai.tock.bot.engine.config.UploadedFilesService.downloadFile import ai.tock.bot.engine.dialog.DialogFlowDAO -import ai.tock.bot.engine.message.Sentence import ai.tock.nlp.admin.AdminVerticle -import ai.tock.nlp.admin.CsvCodec import ai.tock.nlp.admin.model.ApplicationScopedQuery import ai.tock.nlp.admin.model.TranslateReport import ai.tock.nlp.front.client.FrontClient @@ -57,7 +57,6 @@ import ai.tock.translator.I18nLabel import ai.tock.translator.Translator import ai.tock.translator.Translator.initTranslator import ai.tock.translator.TranslatorEngine -import ch.tutteli.kbox.isNotNullAndNotBlank import com.fasterxml.jackson.module.kotlin.readValue import com.github.salomonbrys.kodein.instance import io.vertx.core.http.HttpMethod.GET @@ -74,6 +73,7 @@ open class BotAdminVerticle : AdminVerticle() { private val botAdminConfiguration = BotAdminConfiguration() private val indicatorVerticle = IndicatorVerticle() + private val dialogVerticle = DialogVerticle() override val logger: KLogger = KotlinLogging.logger {} @@ -120,6 +120,7 @@ open class BotAdminVerticle : AdminVerticle() { configureServices() indicatorVerticle.configure(this) + dialogVerticle.configure(this) blockingJsonPost("/users/search", botUser) { context, query: UserSearchQuery -> if (context.organization == query.namespace) { @@ -187,38 +188,38 @@ open class BotAdminVerticle : AdminVerticle() { } blockingJsonPost( - "/analytics/messages/byIntent", + "/analytics/messages/byStory", setOf(botUser) ) { context, request: DialogFlowRequest -> checkAndMeasure(context, request) { - BotAdminAnalyticsService.reportMessagesByIntent(request) + BotAdminAnalyticsService.reportMessagesByStory(request) } } blockingJsonPost( - "/analytics/messages/byDateAndIntent", + "/analytics/messages/byIntent", setOf(botUser) ) { context, request: DialogFlowRequest -> checkAndMeasure(context, request) { - BotAdminAnalyticsService.reportMessagesByDateAndIntent(request) + BotAdminAnalyticsService.reportMessagesByIntent(request) } } blockingJsonPost( - "/analytics/messages/byDateAndStory", + "/analytics/messages/byDateAndIntent", setOf(botUser) ) { context, request: DialogFlowRequest -> checkAndMeasure(context, request) { - BotAdminAnalyticsService.reportMessagesByDateAndStory(request) + BotAdminAnalyticsService.reportMessagesByDateAndIntent(request) } } blockingJsonPost( - "/analytics/messages/byStory", + "/analytics/messages/byDateAndStory", setOf(botUser) ) { context, request: DialogFlowRequest -> checkAndMeasure(context, request) { - BotAdminAnalyticsService.reportMessagesByStory(request) + BotAdminAnalyticsService.reportMessagesByDateAndStory(request) } } @@ -305,117 +306,6 @@ open class BotAdminVerticle : AdminVerticle() { } } - blockingJsonPost( - "/dialogs/ratings/export", - setOf(botUser) - ) { context, query: DialogsSearchQuery -> - if (context.organization == query.namespace) { - val sb = StringBuilder() - val printer = CsvCodec.newPrinter(sb) - printer.printRecord(listOf("Timestamp", "Dialog ID", "Note", "Commentaire")) - BotAdminService.search(query) - .dialogs - .forEach { label -> - printer.printRecord( - listOf( - label.actions.first().date, - label.id, - label.rating, - label.review, - ) - ) - } - sb.toString() - } else { - unauthorized() - } - } - - blockingJsonPost( - "/dialogs/ratings/intents/export", - setOf(botUser) - ) { context, query: DialogsSearchQuery -> - if (context.organization == query.namespace) { - val sb = StringBuilder() - val printer = CsvCodec.newPrinter(sb) - printer.printRecord( - listOf( - "Timestamp", - "Intent", - "Dialog ID", - "Player Type", - "Application ID", - "Message" - ) - ) - BotAdminService.search(query) - .dialogs - .forEach { dialog -> - dialog.actions.forEach { - printer.printRecord( - listOf( - it.date, - it.intent, - dialog.id, - it.playerId.type, - it.applicationId, - if (it.message.isSimpleMessage()) it.message.toPrettyString().replace( - "\n", - " " - ) else (it.message as Sentence).messages.joinToString { it.texts.values.joinToString() } - .replace("\n", " ") - ) - ) - } - } - sb.toString() - - } else { - unauthorized() - } - } - - - blockingJsonGet("/dialog/:applicationId/:dialogId", setOf(botUser)) { context -> - val app = FrontClient.getApplicationById(context.pathId("applicationId")) - if (context.organization == app?.namespace) { - dialogReportDAO.getDialog(context.path("dialogId").toId()) - } else { - unauthorized() - } - } - - blockingJsonPost( - "/dialog/:applicationId/:dialogId/satisfaction", - setOf(botUser) - ) { context, query: Set -> - val app = FrontClient.getApplicationById(context.pathId("applicationId")) - if (context.organization == app?.namespace) { - BotAdminService.getDialogObfuscatedById(context.pathId("dialogId"), query) - } else { - unauthorized() - } - } - - blockingJsonPost( - "/dialogs/search", - setOf(botUser) - ) { context, query: DialogsSearchQuery -> - if (context.organization == query.namespace) { - BotAdminService.search(query) - } else { - unauthorized() - } - } - - blockingJsonGet( - "/dialogs/intents/:applicationId", - setOf(botUser) - ) { context -> - val app = FrontClient.getApplicationById(context.path("applicationId").toId()) - app?.let { BotAdminService.getIntentsInDialogs(app.namespace, app.name) } - } - blockingJsonGet("/bots/:botId", setOf(botUser)) { context -> BotAdminService.getBots(context.organization, context.path("botId")) } diff --git a/bot/admin/server/src/main/kotlin/verticle/DialogVerticle.kt b/bot/admin/server/src/main/kotlin/verticle/DialogVerticle.kt new file mode 100644 index 0000000000..b404883344 --- /dev/null +++ b/bot/admin/server/src/main/kotlin/verticle/DialogVerticle.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.verticle + +import ai.tock.bot.admin.BotAdminService +import ai.tock.bot.admin.BotAdminService.dialogReportDAO +import ai.tock.bot.admin.annotation.BotAnnotationDTO +import ai.tock.bot.admin.annotation.BotAnnotationEventDTO +import ai.tock.bot.admin.annotation.BotAnnotationEventType +import ai.tock.bot.admin.annotation.BotAnnotationUpdateDTO +import ai.tock.bot.admin.model.DialogsSearchQuery +import ai.tock.bot.engine.message.Sentence +import ai.tock.nlp.admin.CsvCodec +import ai.tock.nlp.front.client.FrontClient +import ai.tock.nlp.front.shared.config.ApplicationDefinition +import ai.tock.shared.exception.rest.NotFoundException +import ai.tock.shared.security.TockUser +import ai.tock.shared.security.TockUserRole +import ai.tock.shared.vertx.WebVerticle +import ai.tock.shared.vertx.WebVerticle.Companion.unauthorized +import io.vertx.ext.web.RoutingContext +import org.litote.kmongo.Id +import org.litote.kmongo.toId + +/** + * Verticle handling dialog and annotation related endpoints. + */ +class DialogVerticle { + + companion object { + // DIALOGS ENDPOINTS + private const val PATH_RATINGS_EXPORT = "/dialogs/ratings/export" + private const val PATH_INTENTS_EXPORT = "/dialogs/ratings/intents/export" + private const val PATH_DIALOG = "/dialog/:applicationId/:dialogId" + private const val PATH_DIALOG_SATISFACTION = "/dialog/:applicationId/:dialogId/satisfaction" + private const val PATH_DIALOGS_SEARCH = "/dialogs/search" + private const val PATH_DIALOGS_INTENTS = "/dialogs/intents/:applicationId" + + // ANNOTATION ENDPOINTS + private const val PATH_ANNOTATION = "/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation" + private const val PATH_ANNOTATION_EVENTS = "$PATH_ANNOTATION/:annotationId/events" + private const val PATH_ANNOTATION_EVENT = "/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/events/:eventId" + private const val PATH_ANNOTATION_UPDATE = "/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/:annotationId" + private const val PATH_ANNOTATION_EVENT_DELETE = "/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/:annotationId/events/:eventId" + } + + private val front = FrontClient + + fun configure(webVerticle: WebVerticle) { + with(webVerticle) { + + val currentContextApp: (RoutingContext) -> ApplicationDefinition? = { context -> + val botId = context.pathParam("botId") + val namespace = getNamespace(context) + front.getApplicationByNamespaceAndName( + namespace, botId + ) ?: throw NotFoundException(404, "Could not find $botId in $namespace") + } + + // --------------------------------- Dialog Routes -------------------------------------- + + blockingJsonPost(PATH_RATINGS_EXPORT, setOf(TockUserRole.botUser)) { context, query: DialogsSearchQuery -> + if (context.organization == query.namespace) { + val sb = StringBuilder() + val printer = CsvCodec.newPrinter(sb) + printer.printRecord(listOf("Timestamp", "Dialog ID", "Note", "Commentaire")) + BotAdminService.search(query) + .dialogs + .forEach { label -> + printer.printRecord( + listOf( + label.actions.first().date, + label.id, + label.rating, + label.review, + ) + ) + } + sb.toString() + } else { + unauthorized() + } + } + + blockingJsonPost(PATH_INTENTS_EXPORT, setOf(TockUserRole.botUser)) { context, query: DialogsSearchQuery -> + if (context.organization == query.namespace) { + val sb = StringBuilder() + val printer = CsvCodec.newPrinter(sb) + printer.printRecord( + listOf( + "Timestamp", + "Intent", + "Dialog ID", + "Player Type", + "Application ID", + "Message" + ) + ) + BotAdminService.search(query) + .dialogs + .forEach { dialog -> + dialog.actions.forEach { + printer.printRecord( + listOf( + it.date, + it.intent, + dialog.id, + it.playerId.type, + it.applicationId, + if (it.message.isSimpleMessage()) it.message.toPrettyString().replace( + "\n", + " " + ) else (it.message as Sentence).messages.joinToString { it.texts.values.joinToString() } + .replace("\n", " ") + ) + ) + } + } + sb.toString() + + } else { + unauthorized() + } + } + + blockingJsonGet(PATH_DIALOG, setOf(TockUserRole.botUser)) { context -> + val app = FrontClient.getApplicationById(context.pathId("applicationId")) + if (context.organization == app?.namespace) { + dialogReportDAO.getDialog(context.path("dialogId").toId()) + } else { + unauthorized() + } + } + + blockingJsonPost(PATH_DIALOG_SATISFACTION, setOf(TockUserRole.botUser)) { context, query: Set -> + val app = FrontClient.getApplicationById(context.pathId("applicationId")) + if (context.organization == app?.namespace) { + BotAdminService.getDialogObfuscatedById(context.pathId("dialogId"), query) + } else { + unauthorized() + } + } + + blockingJsonPost(PATH_DIALOGS_SEARCH, setOf(TockUserRole.botUser)) { context, query: DialogsSearchQuery -> + if (context.organization == query.namespace) { + BotAdminService.search(query) + } else { + unauthorized() + } + } + + blockingJsonGet(PATH_DIALOGS_INTENTS, setOf(TockUserRole.botUser)) { context -> + val app = FrontClient.getApplicationById(context.path("applicationId").toId()) + app?.let { BotAdminService.getIntentsInDialogs(app.namespace, app.name) } + } + + // --------------------------------- Annotation Routes ---------------------------------- + + // CREATE ANNO + blockingJsonPost(PATH_ANNOTATION, setOf(TockUserRole.botUser)) { context, annotationDTO: BotAnnotationDTO -> + val dialogId = context.path("dialogId") + val actionId = context.path("actionId") + val user = context.userLogin + + try { + logger.info { "Creating Annotation..." } + BotAdminService.createAnnotation(dialogId, actionId, annotationDTO, user) + } catch (e: IllegalStateException) { + context.fail(400, e) + } + } + + // MODIFY ANNOTATION + blockingJsonPut( + PATH_ANNOTATION_UPDATE, + setOf(TockUserRole.botUser) + ) { context, updatedAnnotationDTO: BotAnnotationUpdateDTO -> + val botId = context.path("botId") + val dialogId = context.path("dialogId") + val actionId = context.path("actionId") + val annotationId = context.path("annotationId") + val user = context.userLogin + + try { + logger.info { "Updating annotation for bot $botId, dialog $dialogId, action $actionId..." } + val updatedAnnotation = BotAdminService.updateAnnotation( + dialogId = dialogId, + actionId = actionId, + annotationId = annotationId, + updatedAnnotationDTO = updatedAnnotationDTO, + user = user + ) + updatedAnnotation + } catch (e: IllegalArgumentException) { + context.fail(400, e) + } catch (e: IllegalStateException) { + context.fail(404, e) + } + } + + // ADD COMMENT + blockingJsonPost( + PATH_ANNOTATION_EVENTS, + setOf(TockUserRole.botUser) + ) { context, eventDTO: BotAnnotationEventDTO -> + val dialogId = context.path("dialogId") + val actionId = context.path("actionId") + val annotationId = context.path("annotationId") + val user = context.userLogin + + if (eventDTO.type != BotAnnotationEventType.COMMENT) { + throw IllegalArgumentException("Only COMMENT events are allowed") + } + + logger.info { "Adding a COMMENT event to annotation $annotationId..." } + BotAdminService.addCommentToAnnotation(dialogId, actionId, eventDTO, user) + } + + // MODIFY COMMENT + blockingJsonPut( + PATH_ANNOTATION_EVENT, + setOf(TockUserRole.botUser) + ) { context, eventDTO: BotAnnotationEventDTO -> + val dialogId = context.path("dialogId") + val actionId = context.path("actionId") + val eventId = context.path("eventId") + val user = context.userLogin + + logger.info { "Modifying a comment..." } + BotAdminService.updateAnnotationEvent(dialogId, actionId, eventId, eventDTO, user) + } + + // DELETE COMMENT + blockingDelete(PATH_ANNOTATION_EVENT_DELETE, setOf(TockUserRole.botUser)) { context -> + val dialogId = context.path("dialogId") + val actionId = context.path("actionId") + val annotationId = context.path("annotationId") + val eventId = context.path("eventId") + val user = context.userLogin + + logger.info { "Deleting a comment..." } + BotAdminService.deleteAnnotationEvent(dialogId, actionId, annotationId, eventId, user) + } + } + } + + /** + * Get the namespace from the context + * @param context : the vertx routing context + */ + private fun getNamespace(context: RoutingContext) = (context.user() as TockUser).namespace + +} diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotation.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotation.kt new file mode 100644 index 0000000000..80fa982d11 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotation.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + + +import org.litote.kmongo.Id +import org.litote.kmongo.newId +import java.time.Instant + +data class BotAnnotation( + val _id: Id = newId(), + val actionId: String, + val dialogId: String, + var state: BotAnnotationState, + var reason: BotAnnotationReasonType?, + var description: String, + var groundTruth: String?, + val events: MutableList, + val createdAt: Instant = Instant.now(), + var lastUpdateDate: Instant = Instant.now(), +) \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationDTO.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationDTO.kt new file mode 100644 index 0000000000..4ca8326b17 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationDTO.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import org.litote.kmongo.newId +import org.litote.kmongo.toId +import java.time.Instant + +data class BotAnnotationDTO( + val id: String? = null, + val state: BotAnnotationState, + val reason: BotAnnotationReasonType? = null, + val description: String, + val groundTruth: String? = null, + val events: List = emptyList() +) \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEvent.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEvent.kt new file mode 100644 index 0000000000..efd1bc99a2 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEvent.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import ai.tock.genai.orchestratorcore.models.Constants +import ai.tock.genai.orchestratorcore.models.llm.AzureOpenAILLMSetting +import ai.tock.genai.orchestratorcore.models.llm.OllamaLLMSetting +import ai.tock.genai.orchestratorcore.models.llm.OpenAILLMSetting +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import org.litote.kmongo.Id +import java.time.Instant + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = BotAnnotationEventComment::class, name = "COMMENT"), + JsonSubTypes.Type(value = BotAnnotationEventChange::class, name = "STATE"), +) +abstract class BotAnnotationEvent ( + open val eventId: Id, + open val type: BotAnnotationEventType, + open val creationDate: Instant, + open val lastUpdateDate: Instant, + open val user: String + ) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventChange.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventChange.kt new file mode 100644 index 0000000000..d4eb8891b5 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventChange.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import org.litote.kmongo.Id +import java.time.Instant + + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = BotAnnotationEventState::class, name = "STATE"), + JsonSubTypes.Type(value = BotAnnotationEventGroundTruth::class, name = "GROUND_TRUTH"), + JsonSubTypes.Type(value = BotAnnotationEventReason::class, name = "REASON"), + JsonSubTypes.Type(value = BotAnnotationEventDescription::class, name = "DESCRIPTION"), +) +abstract class BotAnnotationEventChange( + eventId: Id, + type: BotAnnotationEventType, + creationDate: Instant, + lastUpdateDate: Instant, + user: String, + open val before: String?, + open val after: String? +) : BotAnnotationEvent(eventId, type, creationDate, lastUpdateDate, user) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventComment.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventComment.kt new file mode 100644 index 0000000000..ca61138ed1 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventComment.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import com.fasterxml.jackson.annotation.JsonTypeName +import org.litote.kmongo.Id +import java.time.Instant + +data class BotAnnotationEventComment( + override val eventId: Id, + override val creationDate: Instant, + override val lastUpdateDate: Instant, + override val user: String, + val comment: String +) : BotAnnotationEvent(eventId, BotAnnotationEventType.COMMENT, creationDate, lastUpdateDate, user) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDTO.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDTO.kt new file mode 100644 index 0000000000..fee088e7a1 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDTO.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +data class BotAnnotationEventDTO( + val type: BotAnnotationEventType, + val comment: String? = null, + val before: String?, + val after: String? +) \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDescription.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDescription.kt new file mode 100644 index 0000000000..5bf14c6377 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventDescription.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import org.litote.kmongo.Id +import java.time.Instant + +data class BotAnnotationEventDescription( + override val eventId: Id, + override val creationDate: Instant, + override val lastUpdateDate: Instant, + override val user: String, + override val before: String?, + override val after: String? +) : BotAnnotationEventChange(eventId, BotAnnotationEventType.DESCRIPTION, creationDate, lastUpdateDate, user, before, after) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventGroundTruth.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventGroundTruth.kt new file mode 100644 index 0000000000..738395ee98 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventGroundTruth.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import org.litote.kmongo.Id +import java.time.Instant + +data class BotAnnotationEventGroundTruth( + override val eventId: Id, + override val creationDate: Instant, + override val lastUpdateDate: Instant, + override val user: String, + override val before: String?, + override val after: String? +) : BotAnnotationEventChange(eventId, BotAnnotationEventType.GROUND_TRUTH, creationDate, lastUpdateDate, user, after, before) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventReason.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventReason.kt new file mode 100644 index 0000000000..fc2f36f573 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventReason.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import org.litote.kmongo.Id +import java.time.Instant + +data class BotAnnotationEventReason( + override val eventId: Id, + override val creationDate: Instant, + override val lastUpdateDate: Instant, + override val user: String, + override val before: String?, + override val after: String? +) : BotAnnotationEventChange(eventId, BotAnnotationEventType.REASON, creationDate, lastUpdateDate, user, before, after) diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventState.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventState.kt new file mode 100644 index 0000000000..dde8db8cdd --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +import org.litote.kmongo.Id +import java.time.Instant + +data class BotAnnotationEventState( + override val eventId: Id, + override val creationDate: Instant, + override val lastUpdateDate: Instant, + override val user: String, + override val before: String?, + override val after: String? +) : BotAnnotationEventChange(eventId, BotAnnotationEventType.STATE, creationDate, lastUpdateDate, user, before, after) \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventType.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventType.kt new file mode 100644 index 0000000000..a894d17c94 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationEventType.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +enum class BotAnnotationEventType { + COMMENT, + STATE, + REASON, + GROUND_TRUTH, + DESCRIPTION +} \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationReasonType.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationReasonType.kt new file mode 100644 index 0000000000..5da168746f --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationReasonType.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +enum class BotAnnotationReasonType { + INACCURATE_ANSWER, + INCOMPLETE_ANSWER, + HALLUCINATION, + INCOMPLETE_SOURCES, + OBSOLETE_SOURCES, + WRONG_ANSWER_FORMAT, + BUSINESS_LEXICON_PROBLEM, + QUESTION_MISUNDERSTOOD, + OTHER +} \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationState.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationState.kt new file mode 100644 index 0000000000..dbd2f9dca9 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +enum class BotAnnotationState { + ANOMALY, + REVIEW_NEEDED, + RESOLVED, + WONT_FIX +} \ No newline at end of file diff --git a/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationUpdateDTO.kt b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationUpdateDTO.kt new file mode 100644 index 0000000000..2382a676e1 --- /dev/null +++ b/bot/engine/src/main/kotlin/admin/annotation/BotAnnotationUpdateDTO.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017/2021 e-voyageurs technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tock.bot.admin.annotation + +data class BotAnnotationUpdateDTO( + val state: BotAnnotationState? = null, + val reason: BotAnnotationReasonType? = null, + val description: String? = null, + val groundTruth: String? = null +) diff --git a/bot/engine/src/main/kotlin/admin/dialog/ActionReport.kt b/bot/engine/src/main/kotlin/admin/dialog/ActionReport.kt index a02c55b9db..a75821986f 100644 --- a/bot/engine/src/main/kotlin/admin/dialog/ActionReport.kt +++ b/bot/engine/src/main/kotlin/admin/dialog/ActionReport.kt @@ -16,6 +16,7 @@ package ai.tock.bot.admin.dialog +import ai.tock.bot.admin.annotation.BotAnnotation import ai.tock.bot.connector.ConnectorType import ai.tock.bot.engine.action.Action import ai.tock.bot.engine.action.ActionMetadata @@ -40,5 +41,6 @@ data class ActionReport( val id: Id = newId(), val intent : String?, val applicationId : String?, - val metadata: ActionMetadata + val metadata: ActionMetadata, + val annotation: BotAnnotation? = null ) diff --git a/bot/engine/src/main/kotlin/admin/dialog/DialogReportDAO.kt b/bot/engine/src/main/kotlin/admin/dialog/DialogReportDAO.kt index 815ad52e57..cc1c1a5d2f 100644 --- a/bot/engine/src/main/kotlin/admin/dialog/DialogReportDAO.kt +++ b/bot/engine/src/main/kotlin/admin/dialog/DialogReportDAO.kt @@ -16,6 +16,9 @@ package ai.tock.bot.admin.dialog +import ai.tock.bot.admin.annotation.BotAnnotation +import ai.tock.bot.admin.annotation.BotAnnotationEvent +import ai.tock.bot.admin.annotation.BotAnnotationEventDTO import ai.tock.bot.engine.action.Action import ai.tock.bot.engine.dialog.Dialog import ai.tock.bot.engine.nlp.NlpCallStats @@ -34,4 +37,14 @@ interface DialogReportDAO { fun getDialog(id: Id): DialogReport? fun getNlpCallStats(actionId: Id, namespace: String): NlpCallStats? + + // ANNOTATION FUNCTIONS + fun updateAnnotation(dialogId: String, actionId: String, annotation: BotAnnotation) + fun addAnnotationEvent(dialogId: String, actionId: String, event: BotAnnotationEvent) + fun getAnnotationEvent(dialogId: String, actionId: String, eventId: String): BotAnnotationEvent? + fun updateAnnotationEvent(dialogId: String, actionId: String, eventId: String, updatedEvent: BotAnnotationEvent) + fun deleteAnnotationEvent(dialogId: String, actionId: String, eventId: String) + fun annotationExists(dialogId: String, actionId: String): Boolean + fun getAnnotation(dialogId: String, actionId: String, annotationId: String): BotAnnotation? + fun getAnnotationByActionId(dialogId: String, actionId: String): BotAnnotation? } diff --git a/bot/engine/src/main/kotlin/engine/action/Action.kt b/bot/engine/src/main/kotlin/engine/action/Action.kt index c93b41caf7..892140395a 100644 --- a/bot/engine/src/main/kotlin/engine/action/Action.kt +++ b/bot/engine/src/main/kotlin/engine/action/Action.kt @@ -16,6 +16,7 @@ package ai.tock.bot.engine.action +import ai.tock.bot.admin.annotation.BotAnnotation import ai.tock.bot.definition.ParameterKey import ai.tock.bot.engine.dialog.EventState import ai.tock.bot.engine.event.Event @@ -38,7 +39,8 @@ abstract class Action( id: Id, date: Instant, state: EventState, - val metadata: ActionMetadata = ActionMetadata() + val metadata: ActionMetadata = ActionMetadata(), + var annotation: BotAnnotation? = null ) : Event(applicationId, id, date, state) { abstract fun toMessage(): Message diff --git a/bot/engine/src/main/kotlin/engine/action/SendSentenceWithFootnotes.kt b/bot/engine/src/main/kotlin/engine/action/SendSentenceWithFootnotes.kt index 1446b6d0fe..48aac18033 100644 --- a/bot/engine/src/main/kotlin/engine/action/SendSentenceWithFootnotes.kt +++ b/bot/engine/src/main/kotlin/engine/action/SendSentenceWithFootnotes.kt @@ -16,6 +16,7 @@ package ai.tock.bot.engine.action +import ai.tock.bot.admin.annotation.BotAnnotation import ai.tock.bot.engine.dialog.EventState import ai.tock.bot.engine.message.Message import ai.tock.bot.engine.message.SentenceWithFootnotes @@ -34,9 +35,10 @@ open class SendSentenceWithFootnotes( id: Id = newId(), date: Instant = Instant.now(), state: EventState = EventState(), - metadata: ActionMetadata = ActionMetadata() + metadata: ActionMetadata = ActionMetadata(), + annotation: BotAnnotation? = null ) : - Action(playerId, recipientId, applicationId, id, date, state, metadata) { + Action(playerId, recipientId, applicationId, id, date, state, metadata, annotation) { override fun toMessage(): Message = SentenceWithFootnotes(text.toString(), footnotes.toList()) } diff --git a/bot/storage-mongo/src/main/kotlin/DialogCol.kt b/bot/storage-mongo/src/main/kotlin/DialogCol.kt index c56474341e..8381d2a850 100644 --- a/bot/storage-mongo/src/main/kotlin/DialogCol.kt +++ b/bot/storage-mongo/src/main/kotlin/DialogCol.kt @@ -16,6 +16,7 @@ package ai.tock.bot.mongo +import ai.tock.bot.admin.annotation.* import ai.tock.bot.admin.dialog.ActionReport import ai.tock.bot.admin.dialog.DialogReport import ai.tock.bot.definition.Intent @@ -147,7 +148,8 @@ internal data class DialogCol( a.toActionId(), a.state.intent, a.applicationId, - a.metadata + a.metadata, + a.annotation ) } } @@ -260,6 +262,7 @@ internal data class DialogCol( lateinit var playerId: PlayerId lateinit var recipientId: PlayerId lateinit var applicationId: String + var annotation: BotAnnotation? = null fun assignFrom(action: Action) { id = action.toActionId() @@ -269,6 +272,7 @@ internal data class DialogCol( playerId = action.playerId recipientId = action.recipientId applicationId = action.applicationId + annotation = action.annotation } abstract fun toAction(dialogId: Id): Action @@ -348,7 +352,8 @@ internal data class DialogCol( id, date, state, - botMetadata + botMetadata, + annotation ) } } diff --git a/bot/storage-mongo/src/main/kotlin/UserTimelineMongoDAO.kt b/bot/storage-mongo/src/main/kotlin/UserTimelineMongoDAO.kt index 0912fb7533..946c7c0633 100644 --- a/bot/storage-mongo/src/main/kotlin/UserTimelineMongoDAO.kt +++ b/bot/storage-mongo/src/main/kotlin/UserTimelineMongoDAO.kt @@ -16,6 +16,7 @@ package ai.tock.bot.mongo +import ai.tock.bot.admin.annotation.* import ai.tock.bot.admin.dialog.* import ai.tock.bot.admin.user.* import ai.tock.bot.connector.ConnectorMessage @@ -700,6 +701,147 @@ internal object UserTimelineMongoDAO : UserTimelineDAO, UserReportDAO, DialogRep } } + override fun getAnnotation(dialogId: String, actionId: String, annotationId: String): BotAnnotation? { + val dialog = dialogCol.findOneById(dialogId) ?: return null + for (story in dialog.stories) { + val action = story.actions.find { it.id.toString() == actionId } ?: continue + return action.annotation?.takeIf { it._id.toString() == annotationId } + } + return null + } + + override fun getAnnotationByActionId(dialogId: String, actionId: String): BotAnnotation? { + val dialog = dialogCol.findOneById(dialogId) ?: return null + for (story in dialog.stories) { + val action = story.actions.find { it.id.toString() == actionId } ?: continue + return action.annotation + } + return null + } + + override fun updateAnnotation(dialogId: String, actionId: String, annotation: BotAnnotation) { + val dialog = dialogCol.findOneById(dialogId) + if (dialog != null) { + var annotationUpdated = false + dialog.stories.forEach { story -> + val actionIndex = story.actions.indexOfFirst { it.id.toString() == actionId } + if (actionIndex != -1) { + story.actions[actionIndex].annotation = annotation + annotationUpdated = true + dialogCol.save(dialog) + return + } + } + if (!annotationUpdated) { + logger.warn("Action with ID $actionId not found in dialog $dialogId") + } + } else { + logger.warn("Dialog with ID $dialogId not found") + } + } + + override fun annotationExists(dialogId: String, actionId: String): Boolean { + val dialog = dialogCol.findOneById(dialogId) + return dialog?.stories?.any { story -> + story.actions.any { it.id.toString() == actionId && it.annotation != null } + } ?: false + } + + override fun addAnnotationEvent(dialogId: String, actionId: String, event: BotAnnotationEvent) { + val dialog = dialogCol.findOneById(dialogId) + if (dialog != null) { + var eventAdded = false + dialog.stories.forEach { story -> + val actionIndex = story.actions.indexOfFirst { it.id.toString() == actionId } + if (actionIndex != -1) { + val annotation = story.actions[actionIndex].annotation + if (annotation != null) { + annotation.events.add(event) + annotation.lastUpdateDate = Instant.now() + eventAdded = true + dialogCol.save(dialog) + return + } else { + logger.warn("No annotation found for action $actionId in dialog $dialogId") + } + } + } + if (!eventAdded) { + logger.warn("Action with ID $actionId not found in dialog $dialogId") + } + } else { + logger.warn("Dialog with ID $dialogId not found") + } + } + + override fun getAnnotationEvent(dialogId: String, actionId: String, eventId: String): BotAnnotationEvent? { + val dialog = dialogCol.findOneById(dialogId) ?: return null + for (story in dialog.stories) { + val action = story.actions.find { it.id.toString() == actionId } ?: continue + val annotation = action.annotation ?: return null + return annotation.events.find { it.eventId.toString() == eventId } + } + return null + } + + override fun updateAnnotationEvent(dialogId: String, actionId: String, eventId: String, updatedEvent: BotAnnotationEvent) { + val dialog = dialogCol.findOneById(dialogId) ?: run { + logger.warn("Dialog $dialogId not found") + return + } + var updated = false + for (story in dialog.stories) { + val actionIdx = story.actions.indexOfFirst { it.id.toString() == actionId } + if (actionIdx == -1) continue + val action = story.actions[actionIdx] + val annotation = action.annotation ?: run { + logger.warn("Annotation not found for action $actionId") + return + } + val eventIdx = annotation.events.indexOfFirst { it.eventId.toString() == eventId } + if (eventIdx == -1) { + logger.warn("Event $eventId not found in annotation") + return + } + annotation.events[eventIdx] = updatedEvent + dialogCol.save(dialog) + updated = true + return + } + if (!updated) { + logger.warn("Action $actionId not found in dialog $dialogId") + } + } + + override fun deleteAnnotationEvent(dialogId: String, actionId: String, eventId: String) { + val dialog = dialogCol.findOneById(dialogId) ?: run { + logger.warn("Dialog $dialogId not found") + return + } + var eventDeleted = false + dialog.stories.forEach { story -> + val actionIndex = story.actions.indexOfFirst { it.id.toString() == actionId } + if (actionIndex != -1) { + val annotation = story.actions[actionIndex].annotation ?: run { + logger.warn("Annotation not found for action $actionId") + return + } + val event = annotation.events.find { it.eventId.toString() == eventId } + if (event?.type != BotAnnotationEventType.COMMENT) { + logger.warn("Event $eventId not found or not a comment") + return + } + annotation.events.remove(event) + dialogCol.save(dialog) + eventDeleted = true + return + } + } + if (!eventDeleted) { + logger.warn("Action $actionId not found in dialog $dialogId") + } + } + override fun getArchivedEntityValues( stateValueId: Id, oldActionsMap: Map, Action> diff --git a/docs/_fr/admin/Annotation.md b/docs/_fr/admin/Annotation.md new file mode 100644 index 0000000000..030977010c --- /dev/null +++ b/docs/_fr/admin/Annotation.md @@ -0,0 +1,317 @@ +# Gestion des Annotations et events - DERCBOT-1309 + +**Epic Jira** : [*DERCBOT-1309*](http://go/j/DERCBOT-1309) + + +## Contexte et objectif de la feature + +Ce document de design définit la gestion des annotations et des events liés aux réponses du bot. L'objectif est d'offrir aux administrateurs et développeurs les outils nécessaires pour évaluer, annoter, et tracer les anomalies ainsi que leurs résolutions. + +### Périmètre de la fonctionnalité +Les annotations permettent : +- Aux **administrateur de bot** de marquer une anomalie, de l’analyser et d’y associer des états et raisons spécifiques. +- Aux **administrateur de bot** de filtrer et de suivre les résolutions des anomalies. + +## Cas d'usages + +### En tant qu'administrateur de bot (rôle: botUser) : +* *UC1* - Je souhaite pouvoir **ajouter une annotation** sur une réponse du bot afin d’indiquer un problème. +* *UC2* - Je souhaite pouvoir **modifier une annotation existante** pour refléter les changements d’état, les raisons, ou ajouter des commentaires. +* *UC3* - Je souhaite **suivre l'historique des events liés à une annotation** comme les changements d'état et les commentaires, pour garder une trace complète des décisions. +* *UC4* - Je souhaite pouvoir **filtrer les réponses** en fonction des états et des raisons des anomalies pour identifier les cas nécessitant une attention immédiate. +* *UC5* - Je souhaite pouvoir **modifier** un commentaire existant. +* *UC6* - Je souhaite pouvoir **supprimer** un commentaire existant. +--- + +## Modèle de données + +### Structure et stockage des annotations + +Chaque annotation est un sous-document unique associé à une action spécifique (`actionId`) au sein d'un dialogue (`dialogId`). +- Une action ne peut contenir **qu’une seule annotation** à la fois. +- L’annotation est **nullable**, ce qui signifie qu’une action peut exister sans annotation. +- Lorsqu’une annotation est supprimée, elle est simplement retirée de l’action sans affecter les autres données du dialogue. +- La suppression ou la modification d’une annotation suit la même logique que celle appliquée aux dialogues (ex. expiration alignée sur la purge des dialogues). + + +```mermaid +classDiagram + class Annotation { + +state: AnnotationState + +reason: AnnotationReasonType + +description : String + +ground_truth: String? + +actionId: ObjectID + +dialogId: String + +events: Event[] + +lastUpdateDate: DateTime + } + + class AnnotationEvent { + +eventId: ObjectID + +creationDate: DateTime + +lastUpdateDate: DateTime + +user: String + +type: EventType + } + + class AnnotationEventComment { + +comment: String + } + + class AnnotationEventChange { + +before: String? + +after: String? + } + + class AnnotationEventType { + <> + COMMENT + STATE + REASON + GROUND_TRUTH + DESCRIPTION + } + + class AnnotationState { + <> + ANOMALY + REVIEW_NEEDED + RESOLVED + WONT_FIX + } + + class AnnotationReasonType { + <> + INACCURATE_ANSWER + INCOMPLETE_ANSWER + HALLUCINATION + INCOMPLETE_SOURCES + OBSOLETE_SOURCES + WRONG_ANSWER_FORMAT + BUSINESS_LEXICON_PROBLEM + QUESTION_MISUNDERSTOOD + OTHER + } + + + Annotation "1" *-- "many" AnnotationEvent : contains + AnnotationEvent <|-- AnnotationEventComment : extends + AnnotationEvent <|-- AnnotationEventChange : extends + AnnotationEventType <-- AnnotationEvent : type + AnnotationReasonType <-- Annotation : reason + AnnotationState <-- Annotation : state +``` + +### Exemple de document stocké dans la collection : + +Une purge sera mise sur les annotations, alignée sur la logique de purge des dialogs. + +```json +{ + "annotation": { + "_id": "67980231d6fe5b49dd565613", + "actionId": "6797fc4fe8fd32779aa7cae7", + "dialogId": "6797fc4de8fd32779aa7cae0", + "state": "ANOMALY", + "reason": "INACCURATE_ANSWER", + "description": "Il devrait suggérer de bloquer la carte en urgence.", + "groundTruth": null, + "events": [ + { + "eventId": "67980231d6fe5b49dd565614", + "creationDate": { + "$date": "2025-01-27T22:01:21.853Z" + }, + "lastUpdateDate": { + "$date": "2025-01-27T22:01:21.853Z" + }, + "user": "admin@app.com", + "before": null, + "after": "ANOMALY", + "type": "STATE" + }, + { + "eventId": "6798080853bf0c3beaab8827", + "creationDate": { + "$date": "2025-01-27T22:26:16.237Z" + }, + "lastUpdateDate": { + "$date": "2025-01-27T22:26:16.237Z" + }, + "user": "admin@app.com", + "comment": "Le problème est en cours de traitement", + "type": "COMMENT" + } + ], + "createdAt": { + "$date": "2025-01-27T22:01:21.853Z" + }, + "lastUpdateDate": { + "$date": "2025-01-27T22:26:16.241Z" + } + } +} +``` + +# API Routes Documentation + + +**[POST] /rest/admin/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation** + +Crée une nouvelle annotation. +Une annotation ne peut pas être créée si une annotation existe déjà pour la même `actionId`. + +**Path Parameter** +- `botId` : Identifiant unique du bot. +- `dialogId` : Identifiant unique du dialogue. +- `actionId` : Identifiant unique de l’action. + +**Request Body:** + +- `state`: Obligatoire +- `description`: Obligatoire +- `reason`: Facultatif +- `ground_truth`: Facultatif + +**Corps:** +```json +{ + "state": "ANOMALY", + "description": "Il devrait suggérer de bloquer la carte en urgence.", + "reason": "INACCURATE_ANSWER" +} +``` +**Response:** +```json +{ + "_id": "679b8c1d0d0fbf25765d05f8", + "actionId": "679b7f3fab395066f2740f7e", + "dialogId": "679b7f19ab395066f2740f6c", + "state": "RESOLVED", + "reason": "INACCURATE_ANSWER", + "description": "Test Description", + "groundTruth": "GTTEST", + "events": [ + { + "eventId": "679b8c1d0d0fbf25765d05f9", + "creationDate": "2025-01-30T14:26:37.886678871Z", + "lastUpdateDate": "2025-01-30T14:26:37.886682153Z", + "user": "admin@app.com", + "after": "RESOLVED", + "type": "STATE" + } + ], + "createdAt": "2025-01-30T14:26:37.886665600Z", + "lastUpdateDate": "2025-01-30T14:26:37.886644088Z" +} +``` + +**[POST] /rest/admin/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/:annotationId/events** + +Crée un nouvel event de type comment. + +**Path Parameters** : +- `botId` : Identifiant unique du bot. +- `dialogId` : Identifiant unique du dialogue. +- `actionId` : Identifiant unique de l’action. +- `annotationId` : Identifiant unique de l'annotation. + +**Request Body:** +- `type`: Type de l'event: COMMENT +- `comment`: Commentaire associé à l'event. + +**Corps Example (COMMENT):** +```json +{ + "type": "COMMENT", + "comment": "Le problème est en cours de traitement" +} +``` + +**Response Example (COMMENT):** +```json +{ + "eventId": "67989c8a4efa5148b2818ac8", + "creationDate": "2025-01-28T08:59:54.980977949Z", + "lastUpdateDate": "2025-01-28T08:59:54.980982209Z", + "user": "admin@app.com", + "comment": "Le problème est en cours de traitement", + "type": "COMMENT" +} +``` + +**[PUT] /rest/admin/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/:annotationId** + +Met à jour un event. +On ne peut pas mettre à jour un event de type `comment`. + +Une mise à jour de lastUpdateDate sera faite lors de chaque modification. +Une comparaison est faite sur le back-end entre l'objet stocké sur Mongo et l'objet retourné par le front our déterminer les changements opérés. + +**Path Parameters** : +- `botId` : Identifiant unique du bot. +- `dialogId` : Identifiant unique du dialogue. +- `actionId` : Identifiant unique de l’action. +- `annotationId` : Identifiant unique de l'annotation. + +**Corps Example** +```json +{ + "state": "REVIEW_NEEDED" +} +``` + +**Response Example:** +```json +{ + "_id": "67989c4c4efa5148b2818ac7", + "actionId": "6797fc4fe8fd32779aa7cae7", + "dialogId": "6797fc4de8fd32779aa7cae0", + "state": "REVIEW_NEEDED", + "reason": "INACCURATE_ANSWER", + "description": "Il devrait suggérer de bloquer la carte en urgence.", + "events": [ + { + "eventId": "67989c8a4efa5148b2818ac8", + "creationDate": "2025-01-28T08:59:54.980Z", + "lastUpdateDate": "2025-01-28T08:59:54.980Z", + "user": "admin@app.com", + "comment": "Le problème est en cours de traitement", + "type": "COMMENT" + }, + { + "eventId": "67989dd34efa5148b2818ac9", + "creationDate": "2025-01-28T09:05:23.353635673Z", + "lastUpdateDate": "2025-01-28T09:05:23.353642448Z", + "user": "admin@app.com", + "before": "ANOMALY", + "after": "REVIEW_NEEDED", + "type": "STATE" + } + ], + "createdAt": "2025-01-28T08:58:52.060Z", + "lastUpdateDate": "2025-01-28T09:05:23.353695739Z" +} +``` + +**[DELETE] /rest/admin/bots/:botId/dialogs/:dialogId/actions/:actionId/annotation/events/:eventId** + +Supprime un event. +On ne peut supprimer qu'un event de type `comment`. + +**Path Parameter** +- `botId` : Identifiant unique du bot. +- `dialogId` : Identifiant unique du dialogue. +- `actionId` : Identifiant unique de l’action. +- `annotationId` : Identifiant unique de l'annotation. +- `eventId` : Identifiant unique de l'event. + +**Response Example:** +```json +{ + "message": "Event deleted successfully" +} +``` + +The endpoint /dialogs/search will also reply with the action annotations. \ No newline at end of file diff --git a/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Deserializer.kt b/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Deserializer.kt deleted file mode 100644 index 937abc9462..0000000000 --- a/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Deserializer.kt +++ /dev/null @@ -1,131 +0,0 @@ -package ai.tock.nlp.front.shared.config - -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.module.SimpleModule -import java.time.Instant -import kotlin.Boolean -import kotlin.String -import kotlin.collections.Map -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.full.findParameterByName -import kotlin.reflect.full.primaryConstructor -import org.litote.jackson.JacksonModuleServiceLoader -import org.litote.kmongo.Id - -internal class FaqSettings_Deserializer : JsonDeserializer(), - JacksonModuleServiceLoader { - override fun module() = SimpleModule().addDeserializer(FaqSettings::class.java, this) - - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): FaqSettings { - with(p) { - var __id_: Id? = null - var __id_set : Boolean = false - var _applicationId_: Id? = null - var _applicationId_set : Boolean = false - var _satisfactionEnabled_: Boolean? = null - var _satisfactionEnabled_set : Boolean = false - var _satisfactionStoryId_: String? = null - var _satisfactionStoryId_set : Boolean = false - var _creationDate_: Instant? = null - var _creationDate_set : Boolean = false - var _updateDate_: Instant? = null - var _updateDate_set : Boolean = false - var _token_ : JsonToken? = currentToken - while (_token_?.isStructEnd != true) { - if(_token_ != JsonToken.FIELD_NAME) { - _token_ = nextToken() - if (_token_?.isStructEnd == true) break - } - - val _fieldName_ = currentName - _token_ = nextToken() - when (_fieldName_) { - "_id" -> { - __id_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.readValueAs(__id__reference); - __id_set = true - } - "applicationId" -> { - _applicationId_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.readValueAs(_applicationId__reference); - _applicationId_set = true - } - "satisfactionEnabled" -> { - _satisfactionEnabled_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.booleanValue; - _satisfactionEnabled_set = true - } - "satisfactionStoryId" -> { - _satisfactionStoryId_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.text; - _satisfactionStoryId_set = true - } - "creationDate" -> { - _creationDate_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.readValueAs(Instant::class.java); - _creationDate_set = true - } - "updateDate" -> { - _updateDate_ = if(_token_ == JsonToken.VALUE_NULL) null - else p.readValueAs(Instant::class.java); - _updateDate_set = true - } - else -> { - if (_token_?.isStructStart == true) - p.skipChildren() - nextToken() - } - } - _token_ = currentToken - } - return if(__id_set && _applicationId_set && _satisfactionEnabled_set && - _satisfactionStoryId_set && _creationDate_set && _updateDate_set) - FaqSettings(_id = __id_!!, applicationId = _applicationId_!!, - satisfactionEnabled = _satisfactionEnabled_!!, satisfactionStoryId = - _satisfactionStoryId_, creationDate = _creationDate_!!, updateDate = - _updateDate_!!) - else { - val map = mutableMapOf() - if(__id_set) - map[parameters.getValue("_id")] = __id_ - if(_applicationId_set) - map[parameters.getValue("applicationId")] = _applicationId_ - if(_satisfactionEnabled_set) - map[parameters.getValue("satisfactionEnabled")] = _satisfactionEnabled_ - if(_satisfactionStoryId_set) - map[parameters.getValue("satisfactionStoryId")] = _satisfactionStoryId_ - if(_creationDate_set) - map[parameters.getValue("creationDate")] = _creationDate_ - if(_updateDate_set) - map[parameters.getValue("updateDate")] = _updateDate_ - primaryConstructor.callBy(map) - } - } - } - - companion object { - private val primaryConstructor: KFunction by - lazy(LazyThreadSafetyMode.PUBLICATION) { FaqSettings::class.primaryConstructor!! } - - private val parameters: Map by lazy(LazyThreadSafetyMode.PUBLICATION) { - kotlin.collections.mapOf("_id" to primaryConstructor.findParameterByName("_id")!!, - "applicationId" to primaryConstructor.findParameterByName("applicationId")!!, - "satisfactionEnabled" to - primaryConstructor.findParameterByName("satisfactionEnabled")!!, - "satisfactionStoryId" to - primaryConstructor.findParameterByName("satisfactionStoryId")!!, "creationDate" to - primaryConstructor.findParameterByName("creationDate")!!, "updateDate" to - primaryConstructor.findParameterByName("updateDate")!!) } - - private val __id__reference: TypeReference> = object : - TypeReference>() {} - - private val _applicationId__reference: TypeReference> = object : - TypeReference>() {} - } -} diff --git a/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Serializer.kt b/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Serializer.kt deleted file mode 100644 index fa8aecf7af..0000000000 --- a/nlp/front/storage-mongo/target/generated-sources/kapt/compile/ai/tock/nlp/front/shared/config/FaqSettings_Serializer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package ai.tock.nlp.front.shared.config - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import org.litote.jackson.JacksonModuleServiceLoader - -internal class FaqSettings_Serializer : StdSerializer(FaqSettings::class.java), - JacksonModuleServiceLoader { - override fun module() = SimpleModule().addSerializer(FaqSettings::class.java, this) - - override fun serialize( - value: FaqSettings, - gen: JsonGenerator, - serializers: SerializerProvider - ) { - gen.writeStartObject() - gen.writeFieldName("_id") - val __id_ = value._id - serializers.defaultSerializeValue(__id_, gen) - gen.writeFieldName("applicationId") - val _applicationId_ = value.applicationId - serializers.defaultSerializeValue(_applicationId_, gen) - gen.writeFieldName("satisfactionEnabled") - val _satisfactionEnabled_ = value.satisfactionEnabled - gen.writeBoolean(_satisfactionEnabled_) - gen.writeFieldName("satisfactionStoryId") - val _satisfactionStoryId_ = value.satisfactionStoryId - if(_satisfactionStoryId_ == null) { gen.writeNull() } else { - gen.writeString(_satisfactionStoryId_) - } - gen.writeFieldName("creationDate") - val _creationDate_ = value.creationDate - serializers.defaultSerializeValue(_creationDate_, gen) - gen.writeFieldName("updateDate") - val _updateDate_ = value.updateDate - serializers.defaultSerializeValue(_updateDate_, gen) - gen.writeEndObject() - } -}