diff --git a/DSL/Liquibase/changelog.yaml b/DSL/Liquibase/changelog.yaml index 3c38b8f72..5c157ce0d 100644 --- a/DSL/Liquibase/changelog.yaml +++ b/DSL/Liquibase/changelog.yaml @@ -1,3 +1,3 @@ databaseChangeLog: - includeAll: - path: ./changelog + path: ./changelog/changelog diff --git a/DSL/Liquibase/changelog/20250103092720_add_column_preference.sql b/DSL/Liquibase/changelog/20250103092720_add_column_preference.sql new file mode 100644 index 000000000..fff56d6a9 --- /dev/null +++ b/DSL/Liquibase/changelog/20250103092720_add_column_preference.sql @@ -0,0 +1,4 @@ +-- liquibase formatted sql +-- changeset Vassili Moskaljov:20250103092720 +ALTER TABLE user_page_preferences + ADD COLUMN selected_columns TEXT[] NULL; \ No newline at end of file diff --git a/DSL/Liquibase/changelog/20250905124512_update_intent_status_active_to_trained.sql b/DSL/Liquibase/changelog/20250905124512_update_intent_status_active_to_trained.sql deleted file mode 100644 index d0a3c984f..000000000 --- a/DSL/Liquibase/changelog/20250905124512_update_intent_status_active_to_trained.sql +++ /dev/null @@ -1,16 +0,0 @@ --- liquibase formatted sql --- changeset IgorKrupenja:20250905124512 - --- Create enum type for intent status -CREATE TYPE intent_status AS ENUM ('TRAINED', 'NOT_TRAINED', 'DELETED'); - --- First, increase the column length to accommodate 'NOT_TRAINED' (11 characters) -ALTER TABLE intent ALTER COLUMN status TYPE VARCHAR(15); - --- Update existing data: ACTIVE -> NOT_TRAINED --- Also updates all other possible status values --- Those would actually be invalid but still possible since we were not using an enum previously -UPDATE intent SET status = 'NOT_TRAINED' WHERE status != 'DELETED'; - --- Change column type to use the new enum -ALTER TABLE intent ALTER COLUMN status TYPE intent_status USING status::intent_status; diff --git a/DSL/Liquibase/changelog/rollback/20250905124512_rollback.sql b/DSL/Liquibase/changelog/rollback/20250905124512_rollback.sql deleted file mode 100644 index 274483bc3..000000000 --- a/DSL/Liquibase/changelog/rollback/20250905124512_rollback.sql +++ /dev/null @@ -1,17 +0,0 @@ --- liquibase formatted sql --- changeset IgorKrupenja:20250905124512-rollback - --- Rollback: Revert intent status from enum back to VARCHAR --- This rollback reverses the changes made in 20250905124512_update_intent_status_active_to_trained.sql - --- First, change column type back to VARCHAR -ALTER TABLE intent ALTER COLUMN status TYPE VARCHAR(15); - --- Update data back to original values: NOT_TRAINED -> ACTIVE --- Keep DELETED as is since it was already DELETED -UPDATE intent SET status = 'ACTIVE' WHERE status != 'DELETED'; - -ALTER TABLE intent ALTER COLUMN status TYPE VARCHAR(10); - --- Drop the enum type -DROP TYPE intent_status; diff --git a/DSL/Liquibase/liquibase.properties b/DSL/Liquibase/liquibase.properties index 70e884405..78475ff30 100644 --- a/DSL/Liquibase/liquibase.properties +++ b/DSL/Liquibase/liquibase.properties @@ -1,6 +1,5 @@ changelogFile: ./changelog.yaml -# Below for local use -# url: jdbc:postgresql://database:5432/training_db +# url: jdbc:postgresql://localhost:5432/training_db ; For Local Use url: jdbc:postgresql://171.22.247.13:5434/train_db username: byk password: 01234 diff --git a/DSL/Resql/training/POST/get-intent-last-changed.sql b/DSL/Resql/training/POST/get-intent-last-changed.sql index 72e7e77f9..88dd0b53b 100644 --- a/DSL/Resql/training/POST/get-intent-last-changed.sql +++ b/DSL/Resql/training/POST/get-intent-last-changed.sql @@ -1,5 +1,5 @@ SELECT id, intent, status, created FROM intent -WHERE intent = :intent AND status != 'DELETED' +WHERE intent = :intent AND status = 'ACTIVE' ORDER BY created DESC LIMIT 1; diff --git a/DSL/Resql/training/POST/get-intents-list-last-changed.sql b/DSL/Resql/training/POST/get-intents-list-last-changed.sql index b9461c6f3..5b77b5b2b 100644 --- a/DSL/Resql/training/POST/get-intents-list-last-changed.sql +++ b/DSL/Resql/training/POST/get-intents-list-last-changed.sql @@ -3,7 +3,7 @@ FROM intent i INNER JOIN ( SELECT intent, MAX(created) AS max_created FROM intent - WHERE intent IN (:intentsList) AND status != 'DELETED' + WHERE intent IN (:intentsList) AND status = 'ACTIVE' GROUP BY intent ) subquery ON i.intent = subquery.intent AND i.created = subquery.max_created ORDER BY i.created DESC; diff --git a/DSL/Resql/training/POST/mark-intent-for-service.sql b/DSL/Resql/training/POST/mark-intent-for-service.sql new file mode 100644 index 000000000..75405205c --- /dev/null +++ b/DSL/Resql/training/POST/mark-intent-for-service.sql @@ -0,0 +1,2 @@ +INSERT INTO intent (intent, created, status, isForService) +VALUES (:intent, CURRENT_TIMESTAMP, :status, :isForService); \ No newline at end of file diff --git a/DSL/Resql/training/POST/update-intent-name.sql b/DSL/Resql/training/POST/update-intent-name.sql deleted file mode 100644 index d14fd6bf5..000000000 --- a/DSL/Resql/training/POST/update-intent-name.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Update intent name by inserting a new record with the new name and copying over status and isForService from the old record. --- Expects :oldIntent and :newIntent parameters. -INSERT INTO intent (intent, status, isForService, created) -SELECT - :newIntent, - status, - isForService, - CURRENT_TIMESTAMP -FROM intent -WHERE intent = :oldIntent -ORDER BY created DESC -LIMIT 1; diff --git a/DSL/Resql/training/POST/update-intent.sql b/DSL/Resql/training/POST/update-intent.sql deleted file mode 100644 index 5851d8b25..000000000 --- a/DSL/Resql/training/POST/update-intent.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Insert new row for intent with updated status and/or isForService values --- This finds the latest version of the intent and creates a new row with updated values --- If :status is not provided, keeps the existing status --- If :isForService is not provided, keeps the existing isForService -INSERT INTO intent (intent, created, status, isForService) -SELECT - :intent, - CURRENT_TIMESTAMP, - COALESCE(NULLIF(:status, ''), latest_intent.status), - COALESCE( - CASE - WHEN CAST(:isForService AS TEXT) = '' THEN NULL - ELSE CAST(:isForService AS BOOLEAN) - END, - latest_intent.isForService - ) -FROM ( - -- Find the latest version of the specific intent - SELECT DISTINCT ON (intent) - intent, - status, - isForService, - created - FROM intent - WHERE intent = :intent - ORDER BY intent, created DESC -) AS latest_intent; \ No newline at end of file diff --git a/DSL/Resql/training/POST/update-intents-status.sql b/DSL/Resql/training/POST/update-intents-status.sql deleted file mode 100644 index b06248f3c..000000000 --- a/DSL/Resql/training/POST/update-intents-status.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Insert new rows for multiple intents with specified status --- This finds the latest version of each intent and creates new rows with updated status --- Expects :intents parameter to be an array of intent names and :status to be the new status --- Optional :cutoffDate parameter - if provided, only updates intents where cutoffDate > created -INSERT INTO intent (intent, created, status, isForService) -SELECT - intent_name, - CURRENT_TIMESTAMP, - :status, - latest_intent.isForService -FROM ( - -- Unnest the array of intent names - SELECT unnest(:intents) AS intent_name -) AS intent_list -JOIN ( - -- Find the latest version of each intent - SELECT DISTINCT ON (intent) - intent, - status, - isForService, - created - FROM intent - WHERE intent = ANY(:intents) - ORDER BY intent, created DESC -) AS latest_intent ON latest_intent.intent = intent_list.intent_name -WHERE (:cutoffDate IS NULL OR :cutoffDate > latest_intent.created); diff --git a/DSL/Ruuter.private/training/GET/rasa/intent-ids.yml b/DSL/Ruuter.private/training/GET/rasa/intent-ids.yml index 8008cd117..f002b0b5f 100644 --- a/DSL/Ruuter.private/training/GET/rasa/intent-ids.yml +++ b/DSL/Ruuter.private/training/GET/rasa/intent-ids.yml @@ -39,5 +39,6 @@ mapIntentsData: returnSuccess: return: ${intentsData.response.body} + # todo wrapper: false next: end diff --git a/DSL/Ruuter.private/training/GET/rasa/intents/by-id.yml b/DSL/Ruuter.private/training/GET/rasa/intents/by-id.yml index 6a79967df..31f763c3d 100644 --- a/DSL/Ruuter.private/training/GET/rasa/intents/by-id.yml +++ b/DSL/Ruuter.private/training/GET/rasa/intents/by-id.yml @@ -60,7 +60,7 @@ getIntentListLastChanged: assignResults: assign: - # Ideally, should use a single object, not array + # TODO: Ideally, should use a single object, not array intents: intents: ${getIntentResult.response.body.hits.hits.filter((item) => item._id == intent)} inmodel: ${getDomainDataResult.response.body.response.intents} diff --git a/DSL/Ruuter.private/training/GET/rasa/intents/mark-for-service.yml b/DSL/Ruuter.private/training/GET/rasa/intents/mark-for-service.yml index 4a66fdfeb..5f44469b5 100644 --- a/DSL/Ruuter.private/training/GET/rasa/intents/mark-for-service.yml +++ b/DSL/Ruuter.private/training/GET/rasa/intents/mark-for-service.yml @@ -49,11 +49,11 @@ validateIntentExists: updateInDatabase: call: http.post args: - url: "[#TRAINING_RESQL]/update-intent" + url: "[#TRAINING_RESQL]/mark-intent-for-service" body: intent: ${intent} isForService: ${isForService} - status: "" + status: "ACTIVE" result: addIntentResult returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/intents/add.yml b/DSL/Ruuter.private/training/POST/rasa/intents/add.yml index d3b0895c1..ac0f0d1f5 100644 --- a/DSL/Ruuter.private/training/POST/rasa/intents/add.yml +++ b/DSL/Ruuter.private/training/POST/rasa/intents/add.yml @@ -92,7 +92,7 @@ addInDatabase: url: "[#TRAINING_RESQL]/add-intent" body: intent: ${incoming.body.name} - status: "NOT_TRAINED" + status: "ACTIVE" result: addIntentResult returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/intents/examples/add.yml b/DSL/Ruuter.private/training/POST/rasa/intents/examples/add.yml index be1c81f03..156014f7e 100644 --- a/DSL/Ruuter.private/training/POST/rasa/intents/examples/add.yml +++ b/DSL/Ruuter.private/training/POST/rasa/intents/examples/add.yml @@ -188,7 +188,7 @@ addInDatabase: url: "[#TRAINING_RESQL]/add-intent" body: intent: ${incoming.body.intentName} - status: "NOT_TRAINED" + status: "ACTIVE" result: addInDatabaseResult returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/intents/examples/delete.yml b/DSL/Ruuter.private/training/POST/rasa/intents/examples/delete.yml index b42b490ad..757f76598 100644 --- a/DSL/Ruuter.private/training/POST/rasa/intents/examples/delete.yml +++ b/DSL/Ruuter.private/training/POST/rasa/intents/examples/delete.yml @@ -152,7 +152,7 @@ addInDatabase: url: "[#TRAINING_RESQL]/add-intent" body: intent: ${incoming.body.intentName} - status: "NOT_TRAINED" + status: "ACTIVE" result: addInDatabaseResult saveIntentFile: diff --git a/DSL/Ruuter.private/training/POST/rasa/intents/examples/update.yml b/DSL/Ruuter.private/training/POST/rasa/intents/examples/update.yml index 372593ab9..88ca57f18 100644 --- a/DSL/Ruuter.private/training/POST/rasa/intents/examples/update.yml +++ b/DSL/Ruuter.private/training/POST/rasa/intents/examples/update.yml @@ -150,7 +150,7 @@ addInDatabase: url: "[#TRAINING_RESQL]/add-intent" body: intent: ${incoming.body.intentName} - status: "NOT_TRAINED" + status: "ACTIVE" result: addInDatabaseResult saveIntentFile: diff --git a/DSL/Ruuter.private/training/POST/rasa/intents/update.yml b/DSL/Ruuter.private/training/POST/rasa/intents/update.yml index e17b22299..11e39e900 100644 --- a/DSL/Ruuter.private/training/POST/rasa/intents/update.yml +++ b/DSL/Ruuter.private/training/POST/rasa/intents/update.yml @@ -291,10 +291,10 @@ addNewIntentInPipeline: addInDatabase: call: http.post args: - url: "[#TRAINING_RESQL]/update-intent-name" + url: "[#TRAINING_RESQL]/add-intent" body: - oldIntent: ${incoming.body.oldName} - newIntent: ${incoming.body.newName} + intent: ${incoming.body.newName} + status: "ACTIVE" result: addInDatabaseResult deleteOldIntentFile: diff --git a/DSL/Ruuter.private/training/POST/rasa/model/trained-model.yml b/DSL/Ruuter.private/training/POST/rasa/model/trained-model.yml index b09bf720e..ec150ed00 100644 --- a/DSL/Ruuter.private/training/POST/rasa/model/trained-model.yml +++ b/DSL/Ruuter.private/training/POST/rasa/model/trained-model.yml @@ -13,10 +13,7 @@ declaration: description: "Body field 'versionNumber'" - field: name type: string - description: "Trained model file name" - - field: previousModelName - type: string - description: "Model name that was previously active" + description: "Trained model file name" getModelByFilenameFromDb: call: http.post @@ -42,44 +39,6 @@ updateInDatabase: training_data_checksum: "" result: dbModelResult -getPreviousModelFromDb: - call: http.post - args: - url: "[#TRAINING_RESQL]/get-llm-model-by-filename" - body: - fileName: ${incoming.body.previousModelName} - result: previousModelResult - -compareModelIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/compare-model-intent-reports" - body: - newModelReport: ${dbResult.response.body[0].crossValidationReport.intent_evaluation.report} - oldModelReport: ${previousModelResult.response.body[0].crossValidationReport.intent_evaluation.report} - result: compareModelIntentsResult - -updateOldIntentsStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intents-status" - body: - intents: ${compareModelIntentsResult.oldModelUniqueIntents} - status: "NOT_TRAINED" - result: updateOldIntentsResult - -updateNewIntentsStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intents-status" - body: - intents: ${compareModelIntentsResult.newModelUniqueIntents} - status: "TRAINED" - # Only update intents that were last modified before the new model was trained - # Otherwise, the model needs to be trained again to reflect changes in intents - cutoffDate: ${dbResult.response.body[0].trainedDate} - result: updateNewIntentsResult - loadTrainedModel: call: http.post args: diff --git a/DSL/Ruuter.private/training/POST/rasa/responses/add.yml b/DSL/Ruuter.private/training/POST/rasa/responses/add.yml index 63087041a..cd1f2d5b6 100644 --- a/DSL/Ruuter.private/training/POST/rasa/responses/add.yml +++ b/DSL/Ruuter.private/training/POST/rasa/responses/add.yml @@ -14,9 +14,6 @@ declaration: - field: response type: string description: "Body field 'response'" - - field: intent - type: string - description: "Intent name for the response" header: - field: cookie type: string @@ -89,25 +86,8 @@ updateOpenSearch: args: url: "[#TRAINING_PIPELINE]/bulk/domain" body: - input: ${domainYaml.response.body.json} + input: ${domainYaml.response.body.json} result: updateSearchResult - next: isIntentProvided - -isIntentProvided: - switch: - - condition: ${incoming.body.intent} - next: updateIntentStatus - next: returnSuccess - -updateIntentStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intent" - body: - intent: ${intent} - isForService: "" - status: "NOT_TRAINED" - result: updateIntentStatusResult next: returnSuccess returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/responses/update.yml b/DSL/Ruuter.private/training/POST/rasa/responses/update.yml index 7552ce652..254f95b56 100644 --- a/DSL/Ruuter.private/training/POST/rasa/responses/update.yml +++ b/DSL/Ruuter.private/training/POST/rasa/responses/update.yml @@ -14,9 +14,6 @@ declaration: - field: response type: object description: "Body field 'response'" - - field: intent - type: string - description: "Intent name for the response" header: - field: cookie type: string @@ -91,23 +88,6 @@ updateOpenSearch: body: input: ${domainYaml.response.body.json} result: updateSearchResult - next: isIntentProvided - -isIntentProvided: - switch: - - condition: ${incoming.body.intent} - next: updateIntentStatus - next: returnSuccess - -updateIntentStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intent" - body: - intent: ${intent} - isForService: "" - status: "NOT_TRAINED" - result: updateIntentStatusResult next: returnSuccess returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/rules/add.yml b/DSL/Ruuter.private/training/POST/rasa/rules/add.yml index 215974d03..46d9ec914 100644 --- a/DSL/Ruuter.private/training/POST/rasa/rules/add.yml +++ b/DSL/Ruuter.private/training/POST/rasa/rules/add.yml @@ -91,8 +91,6 @@ mergeRules: iteratee: "rule" result: mergedRules -# todo also: rules add AND update AND delete - under rules UI, check if add and remove intents to rules - convertJsonToYaml: call: http.post args: @@ -120,25 +118,6 @@ updateOpenSearch: body: input: ${rulesYaml.response.body.json} result: updateSearchResult - next: getRuleIntents - -getRuleIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/get-intents-from-rule-steps" - body: - steps: ${body.steps} - result: ruleIntentsResult - next: updateIntentsStatus - -updateIntentsStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intents-status" - body: - intents: ${ruleIntentsResult.response.body} - status: "NOT_TRAINED" - result: updateIntentsResult next: returnSuccess returnSuccess: diff --git a/DSL/Ruuter.private/training/POST/rasa/rules/delete.yml b/DSL/Ruuter.private/training/POST/rasa/rules/delete.yml index 6c8a1b623..778dd7547 100644 --- a/DSL/Ruuter.private/training/POST/rasa/rules/delete.yml +++ b/DSL/Ruuter.private/training/POST/rasa/rules/delete.yml @@ -108,26 +108,6 @@ deleteOpenSearchEntry: body: id: ${rule} result: deleteSearchResult - next: getRuleIntents - -getRuleIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/get-intents-from-rule-steps" - body: - steps: ${ruleResult.response.body.response.steps} - result: ruleIntentsResult - next: updateIntentsStatus - -updateIntentsStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intents-status" - body: - intents: ${ruleIntentsResult.response.body} - status: "NOT_TRAINED" - result: updateIntentsResult - next: returnSuccess returnSuccess: return: "Rule deleted" diff --git a/DSL/Ruuter.private/training/POST/rasa/rules/update.yml b/DSL/Ruuter.private/training/POST/rasa/rules/update.yml index ced678d1e..2e4b2f60a 100644 --- a/DSL/Ruuter.private/training/POST/rasa/rules/update.yml +++ b/DSL/Ruuter.private/training/POST/rasa/rules/update.yml @@ -119,47 +119,6 @@ updateOpenSearch: body: input: ${rulesYaml.response.body.json} result: updateSearchResult - next: getOldRuleIntents - -getOldRuleIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/get-intents-from-rule-steps" - body: - steps: ${ruleResult.response.body.response.steps} - result: oldRuleIntentsResult - next: getNewRuleIntents - -getNewRuleIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/get-intents-from-rule-steps" - body: - steps: ${ruleData.steps} - result: newRuleIntentsResult - next: compareIntents - -compareIntents: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/utils/compare-model-intent-reports" - body: - oldArray: ${oldRuleIntentsResult.response.body} - newArray: ${newRuleIntentsResult.response.body} - result: compareIntentsResult - next: updateIntentsStatus - -updateIntentsStatus: - call: http.post - args: - url: "[#TRAINING_RESQL]/update-intents-status" - body: - # Setting NOT_TRAINED for intents that: - # - have been added to the rule - # - have been removed from the rule - intents: ${[...compareIntentsResult.response.body.newUniqueItems, ...compareIntentsResult.response.body.oldUniqueItems]} - status: "NOT_TRAINED" - result: updateIntentsResult next: returnSuccess returnSuccess: diff --git a/GUI/src/pages/ModelBankAndAnalytics/IntentsOverview/index.tsx b/GUI/src/pages/ModelBankAndAnalytics/IntentsOverview/index.tsx index d41f067d5..844fbd4f0 100644 --- a/GUI/src/pages/ModelBankAndAnalytics/IntentsOverview/index.tsx +++ b/GUI/src/pages/ModelBankAndAnalytics/IntentsOverview/index.tsx @@ -1,142 +1,132 @@ -import { FC, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useQuery } from '@tanstack/react-query'; -import { MdOutlineSettingsInputAntenna } from 'react-icons/md'; - -import { Card, DataTable, FormInput, FormSelect, Icon, Track } from 'components'; -import { Metrics, IntentsReport } from 'types/intentsReport'; -import { Model } from 'types/model'; +import {FC, useEffect, useMemo, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {useQuery} from '@tanstack/react-query'; +import {MdOutlineSettingsInputAntenna} from 'react-icons/md'; + +import {Card, DataTable, FormInput, FormSelect, Icon, Track} from 'components'; +import {IntentsReport} from 'types/intentsReport'; +import {Model} from 'types/model'; import { getColumns } from './columns'; import withAuthorization, { ROLES } from 'hoc/with-authorization'; import { isHiddenFeaturesEnabled } from 'constants/config'; const IntentsOverview: FC = () => { - const { t } = useTranslation(); - const [filter, setFilter] = useState(''); - const [selectedModelId, setSelectedModelId] = useState(''); - - const { data: models } = useQuery({ - queryKey: ['models'], - }); - - const [modelInUse, setModelInUse] = useState(false); - const [trainedDate, setTrainedDate] = useState(''); - const [accuracyValue, setAccuracyValue] = useState(0); - - const { data: intentsReport, refetch } = useQuery({ - queryKey: [`model/get-report-by-name?fileName=${selectedModelId}`], - enabled: false, - }); - const nonIntents = isHiddenFeaturesEnabled ? ['accuracy', 'macro avg', 'weighted avg', 'micro avg'] : []; - - useEffect(() => { - if (!models) return; - let deployed = models.find((model) => model.state === 'DEPLOYED'); - if (!deployed) deployed = models.find((model) => model.state === 'READY'); - if (!deployed) deployed = models?.[0]; - setSelectedModelId(deployed?.id || 0); - }, [models]); - - const modelsOptions = useMemo(() => { - if (!models) return []; - return models.map((model) => ({ label: model.name, value: String(model.id) })); - }, [models]); - - useEffect(() => { - if (!selectedModelId) return; - refetch(); - setStatesById(selectedModelId); - }, [selectedModelId]); - - useEffect(() => { - if (intentsReport?.intent_evaluation?.report) { - setAccuracyValue(intentsReport?.intent_evaluation?.report['accuracy']); - } - }, [intentsReport]); - - const formattedIntentsReport = useMemo( - () => - intentsReport - ? Object.keys(intentsReport.intent_evaluation.report).map((intent) => { - const report = intentsReport.intent_evaluation.report; - const metrics = report[intent] as Metrics; - return { - intent, - ...metrics, - }; - }) - : [], - [] - ); - - const intentsReportColumns = useMemo(() => getColumns({ accuracyValue, nonIntents }), [accuracyValue]); - - const setStatesById = (modelId: string) => { - if (!models) { - return; - } - const selectedModel = models.find((m) => m.name === modelId); - setModelInUse(selectedModel?.state.toUpperCase() === 'DEPLOYED'); - const date = selectedModel?.lastTrained.split('T')[0] ?? ''; - const [year, month, day] = date.split('-'); - setTrainedDate(`${day}.${month}.${year}`); - }; - - return ( - <> -

{t('training.mba.intentsOverview')}

- - - - {models && ( - { - refetch(); - setSelectedModelId(model?.value ?? ''); - setStatesById(model?.value ?? ''); - }} - /> - )} - {modelInUse && ( - - } size="medium" /> -

{t('training.mba.modelInUse')}

- - )} - {trainedDate && ( -

- {t('training.mba.trained')}: {trainedDate} -

- )} - -
- - setFilter(e.target.value)} - /> + const {t} = useTranslation(); + const [filter, setFilter] = useState(''); + const [selectedModelId, setSelectedModelId] = useState(''); + + const {data: models} = useQuery({ + queryKey: ['models'], + }); + + const [modelInUse, setModelInUse] = useState(false); + const [trainedDate, setTrainedDate] = useState('') + const [accuracyValue, setAccuracyValue] = useState(0); + + const {data: intentsReport, refetch} = useQuery({ + queryKey: [`model/get-report-by-name?fileName=${selectedModelId}`], + enabled: false, + }); + const nonIntents = isHiddenFeaturesEnabled ? ['accuracy','macro avg','weighted avg', 'micro avg'] : []; + + useEffect(() => { + if (!models) return; + let deployed = models.find((model) => model.state === 'DEPLOYED'); + if (!deployed) + deployed = models.find((model) => model.state === 'READY'); + if (!deployed) + deployed = models?.[0]; + setSelectedModelId(deployed?.id || 0); + }, [models]) + + const modelsOptions = useMemo(() => { + if (!models) return []; + return models.map((model) => ({label: model.name, value: String(model.id)})); + }, [models]) + + useEffect(() => { + if (!selectedModelId) return; + refetch(); + setStatesById(selectedModelId); + }, [selectedModelId]) + + useEffect(() => { + if (intentsReport?.intent_evaluation?.report) { + setAccuracyValue(intentsReport?.intent_evaluation?.report['accuracy']); } - > - - - - ); + }, [intentsReport]); + + const formattedIntentsReport = useMemo( + () => intentsReport + ? Object.keys(intentsReport.intent_evaluation.report).map((intent) => ({intent, ...intentsReport.intent_evaluation.report[intent]})) + : [], + [intentsReport], + ); + + const intentsReportColumns = useMemo(() => getColumns({ accuracyValue, nonIntents}), [accuracyValue]); + + const setStatesById = (modelId: string) => { + if (!models) { + return; + } + const selectedModel = models.find((m) => m.name === modelId) + setModelInUse(selectedModel?.state.toUpperCase() === 'DEPLOYED'); + const date = selectedModel?.lastTrained.split('T')[0] ?? ''; + const [year, month, day] = date.split("-"); + setTrainedDate(`${day}.${month}.${year}`) + } + + + return ( + <> +

{t('training.mba.intentsOverview')}

+ + + + {models && ( + { + refetch(); + setSelectedModelId(model?.value ?? '') + setStatesById(model?.value ?? '') + } + } + /> + )} + {modelInUse && + } size='medium'/> +

{t('training.mba.modelInUse')}

+ } + {trainedDate &&

+ {t('training.mba.trained')}: {trainedDate} +

} + +
+ + setFilter(e.target.value)} + /> + }> + + + + ); }; export default withAuthorization(IntentsOverview, [ diff --git a/GUI/src/pages/ModelBankAndAnalytics/Models/index.tsx b/GUI/src/pages/ModelBankAndAnalytics/Models/index.tsx index 70fdf64e1..b4bbbbbb7 100644 --- a/GUI/src/pages/ModelBankAndAnalytics/Models/index.tsx +++ b/GUI/src/pages/ModelBankAndAnalytics/Models/index.tsx @@ -5,9 +5,17 @@ import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; import { AxiosError } from 'axios'; import { MdOutlineSettingsInputAntenna } from 'react-icons/md'; -import './Models.scss'; +import './Models.scss' -import { Button, Card, DataTable, Dialog, Icon, Loader, Track } from 'components'; +import { + Button, + Card, + DataTable, + Dialog, + Icon, + Loader, + Track, +} from 'components'; import { Model, ModelStateType, UpdateModelDTO } from 'types/model'; import { activateModel, deleteModel } from 'services/models'; import { useToast } from 'hooks/useToast'; @@ -26,8 +34,12 @@ const Models: FC = () => { const [currentlyLoadedModel, setCurrentlyLoadedModel] = useState(); const [previouslyLoadedModel, setPreviouslyLoadedModel] = useState(); const [isFetching, setIsFetching] = useState(false); - const [modelConfirmation, setModelConfirmation] = useState(null); - const [deletableModel, setDeletableModel] = useState(null); + const [modelConfirmation, setModelConfirmation] = useState< + string | number | null + >(null); + const [deletableModel, setDeletableModel] = useState( + null + ); const { data: models, refetch } = useQuery({ queryKey: ['models'], }); @@ -79,7 +91,7 @@ const Models: FC = () => { setCurrentlyLoadedModel(deployedModel); setPreviouslyLoadedModel(deployedModel); - const activatingModel = models?.find((m) => m.state === 'ACTIVATING'); + const activatingModel = models?.find((m) => m.state === 'ACTIVATING') if (activatingModel) { setSelectedModel(activatingModel); setCurrentlyLoadedModel(activatingModel); @@ -117,23 +129,37 @@ const Models: FC = () => { clearTimeout(timeoutId); }; } - }, [isFetching, refetch, currentlyLoadedModel, previouslyLoadedModel, toast, t]); + }, [ + isFetching, + refetch, + currentlyLoadedModel, + previouslyLoadedModel, + toast, + t, + ]); return ( <>

{t('training.mba.models')}

{selectedModel && ( - {t('training.mba.selectedModel')}}> + {t('training.mba.selectedModel')}} + >

- {`${i18n.t('training.mba.version')} ${selectedModel.versionNumber}`} + {`${i18n.t('training.mba.version')} ${ + selectedModel.versionNumber + }`}

{`(${selectedModel.name})`}

{selectedModel.state === 'DEPLOYED' && ( - } size="medium" /> + } + size="medium" + />

{t('training.mba.modelInUse')}

)} @@ -148,8 +174,13 @@ const Models: FC = () => { {selectedModel.state !== 'DEPLOYED' && ( - - @@ -208,7 +244,10 @@ const Models: FC = () => { onClose={() => setModelConfirmation(null)} footer={ <> - - ) : ( - - )} - -

- {t('global.modifiedAt')}: - {isValidDate(intent.modifiedAt) - ? ` ${format(new Date(intent.modifiedAt), 'dd.MM.yyyy')}` - : ` ${t('global.missing')}`} -

- - {serviceEligible() && ( - - updateMarkForService(value)} - checked={intent.isForService} - disabled={isPossibleToUpdateMark} - /> - - )} - - - - {intent.inModel ? ( - - ) : ( - - )} - {isHiddenFeaturesEnabled && serviceEligible() && ( - + onSuccess: () => { + if (intent?.inModel === true) { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.intentRemovedFromModel'), + }); + } else { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.intentAddedToModel'), + }); + } + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setEditingIntentTitle(null); + setRefreshing(false); + queryRefresh(); + }, + }); + + const addOrEditResponseMutation = useMutation({ + mutationFn: (intentResponseData: { id: string; responseText: string; update: boolean }) => + editResponse(intentResponseData.id, intentResponseData.responseText, intentResponseData.update), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: [`intents/is-marked-for-service?intent=${intentId}`], + refetchType: 'all', + }); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.newResponseAdded'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + queryRefresh(); + setRefreshing(false); + }, + }); + + const addRuleMutation = useMutation({ + mutationFn: ({data}: { data: RuleDTO }) => addRule(data), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: () => { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.ruleAdded'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setRefreshing(false); + }, + }); + + const handleIntentResponseSubmit = async () => { + if (response.text === '' || !intent) return; + + const intentId = intent.id; + + addOrEditResponseMutation.mutate({ + id: `utter_${intentId}`, + responseText: response.text, + update: !!response.name, + }); + + if (!response.name) { + addRuleMutation.mutate({ + data: { + rule: `rule${intentId}`, + steps: [ + { + intent: intentId, + }, + { + action: `utter_${intentId}`, + }, + ], + }, + }); + } + }; + + const deleteIntentMutation = useMutation({ + mutationFn: (name: string) => deleteIntent({name}), + onMutate: () => { + setRefreshing(true); + setShowDeleteModal(false); + setShowConnectToServiceModal(false); + }, + onSuccess: async () => { + setListIntents((prev) => prev.filter((intent) => intent.id !== intentId)); + setSelectedIntent(null); + + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.intentDeleted'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const updateSelectedIntent = (updatedIntent: Intent) => { + setSelectedIntent(null); + setTimeout(() => setSelectedIntent(updatedIntent), 20); + }; + + useDocumentEscapeListener(() => setEditingIntentTitle(null)); + + if (!intent) return <>Loading...; + + return ( + +
+ + + + {editingIntentTitle ? ( + { + const value = e.target.value; + const hasSpecialCharacters = /[^\p{L}\p{N} ]/u; + if (!hasSpecialCharacters.test(value) && !value.startsWith(' ')) { + setEditingIntentTitle(e.target.value); + } + }} + hideLabel + /> + ) : ( +

{intent.id?.replace(/_/g, ' ')}

+ )} + {editingIntentTitle ? ( + + ) : ( + + )} + +

+ {t('global.modifiedAt')}: + {isValidDate(intent.modifiedAt) + ? ` ${format(new Date(intent.modifiedAt), 'dd.MM.yyyy')}` + : ` ${t('global.missing')}`} +

+ + {serviceEligible() && ( + + updateMarkForService(value)} + checked={intent.isForService} + disabled={isPossibleToUpdateMark} + /> + + )} + + + + {intent.inModel ? ( + + ) : ( + + )} + {isHiddenFeaturesEnabled && serviceEligible() && ( + - - )} - + )} + - - -
- -
- {intent?.examples && ( - -
-
-

{t('training.intents.responseTitle')}

- - { - if (!withBackSlash.test(e.target.value) && !e.target.value.startsWith(' ')) { - setResponse({ - name: response?.name ?? '', - text: e.target.value ?? '', - }); - } else { - e.target.value = response.text; - } - }} - /> - + + -
-
- - )} -
- - {showDeleteModal && ( - setShowDeleteModal(false)} - footer={ - <> - - - - } - > -

{t('global.removeValidation')}

-
- )} - - {showConnectToServiceModal && ( - setShowConnectToServiceModal(false)} /> - )} - - {refreshing && ( - -

{t('global.updatingDataBody')}

-
- )} -
- ); + +
+ {intent?.examples && ( + +
+
+

{t('training.intents.responseTitle')}

+ + { + if (!withBackSlash.test(e.target.value) && !e.target.value.startsWith(' ')) { + setResponse({ + name: response?.name ?? '', + text: e.target.value ?? '', + }); + } else { + e.target.value = response.text; + } + }} + /> + + +
+ +
+ + )} +
+ + {showDeleteModal && ( + setShowDeleteModal(false)} + footer={ + <> + + + + } + > +

{t('global.removeValidation')}

+
+ )} + + {showConnectToServiceModal && ( + setShowConnectToServiceModal(false)}/> + )} + + {refreshing && ( + +

{t('global.updatingDataBody')}

+
+ )} + + ); }; export default IntentDetails; diff --git a/GUI/src/pages/Training/Rules/RulesDetail.tsx b/GUI/src/pages/Training/Rules/RulesDetail.tsx index 9919f0976..1e17a6a27 100644 --- a/GUI/src/pages/Training/Rules/RulesDetail.tsx +++ b/GUI/src/pages/Training/Rules/RulesDetail.tsx @@ -81,7 +81,6 @@ const RulesDetail: FC<{ mode: 'new' | 'edit' }> = ({ mode }) => { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const onConnect = useCallback((params: Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]); - const [initialIntents, setInitialIntents] = useState([]); const { data: currentEntityData, refetch: refetchCurrentEntity } = useQuery({ queryKey: ['rule-by-name', currentEntityId], diff --git a/GUI/src/services/responses.ts b/GUI/src/services/responses.ts index 9aeb63fed..13f3fbfec 100644 --- a/GUI/src/services/responses.ts +++ b/GUI/src/services/responses.ts @@ -1,6 +1,6 @@ import { PaginationParams } from 'types/api'; import { rasaApi } from './api'; -import { ResponseEdit, Response } from 'types/response'; +import { ResponseEdit, ResponseDataEdit, Response } from 'types/response'; export const getResponses = async ({ pageParam, @@ -11,22 +11,24 @@ export const getResponses = async ({ return data; }; -export async function editResponse(id: string, responseText: string, update = true, intent?: string) { +export async function editResponse(id: string, responseText: string, update = true) { if (responseText.startsWith('"') && responseText.endsWith('"')) { responseText = responseText.slice(1, -1); } + const responseEditData = {}; + const responseDataEdit = {}; - const responseEditData: ResponseEdit = { - response_name: id, - response: { - [id]: [{ text: responseText }], - }, - intent, - }; + responseEditData.response_name = id; + responseDataEdit[id] = [{ text: responseText }]; + responseEditData.response = responseDataEdit; - const endpoint = update ? 'responses/update' : 'responses/add'; - const { data } = await rasaApi.post<{ response: string }>(endpoint, responseEditData); - return data; + if (update) { + const { data } = await rasaApi.post<{ response: string }>(`responses/update`, responseEditData); + return data; + } else { + const { data } = await rasaApi.post<{ response: string }>(`responses/add`, responseEditData); + return data; + } } export async function deleteResponse(response: { response: string }) { diff --git a/GUI/src/types/intentsReport.ts b/GUI/src/types/intentsReport.ts index 0e757ce70..8360b8194 100644 --- a/GUI/src/types/intentsReport.ts +++ b/GUI/src/types/intentsReport.ts @@ -1,4 +1,4 @@ -export interface Metrics { +export interface IntentsReportResult { precision: number; recall: number; 'f1-score': number; @@ -6,32 +6,24 @@ export interface Metrics { confused_with?: Record; } -interface IntentsReportResult { - accuracy: number; - 'macro avg': Metrics; - 'weighted avg': Metrics; - 'micro avg': Metrics; - [intentName: string]: Metrics | number; -} - interface EvaluationResult { - report: IntentsReportResult; - precision: number | null; - f1_score: number | null; + report: IntentsReportResult, + precision: number; + f1_score: number; errors: { text: string; intent: string; intent_prediction: { name: string; confidence: number; - }; - }[]; + } + } } export interface IntentsReport { - intent_evaluation: EvaluationResult; - entity_evaluation: EvaluationResult; - response_selection_evaluation: EvaluationResult; + intent_evaluation: EvaluationResult, + entity_evaluation: EvaluationResult, + response_selection_evaluation: EvaluationResult, } -export type IntentReport = Metrics & { intent: string }; +export type IntentReport = IntentsReportResult & { intent: string }; diff --git a/GUI/src/types/response.ts b/GUI/src/types/response.ts index 94ca08843..c7075ce2b 100644 --- a/GUI/src/types/response.ts +++ b/GUI/src/types/response.ts @@ -9,14 +9,13 @@ export interface Condition { value: null; } -interface ResponseDataEdit { +export interface ResponseDataEdit { [key: string]: ResponseDataResponse[]; } export interface ResponseEdit { response_name: string; response: ResponseDataEdit; - intent?: string; } export interface Response { diff --git a/migrate.sh b/migrate.sh index 5c9798742..d659cd6d7 100755 --- a/migrate.sh +++ b/migrate.sh @@ -1,2 +1,2 @@ #!/bin/bash -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase:/liquibase/changelog -w /liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties update +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase:/liquibase/changelog liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties update