From b639f87d1db9dfe1b1bc102a535439933afd6961 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:20:02 +0200 Subject: [PATCH 01/14] First attempt at creating the UserModel class --- moviebot/user_modeling/__init__.py | 0 moviebot/user_modeling/user_model.py | 108 +++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 moviebot/user_modeling/__init__.py create mode 100644 moviebot/user_modeling/user_model.py diff --git a/moviebot/user_modeling/__init__.py b/moviebot/user_modeling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py new file mode 100644 index 0000000..296ea6c --- /dev/null +++ b/moviebot/user_modeling/user_model.py @@ -0,0 +1,108 @@ +"""Class for user modeling.""" + + +from collections import defaultdict +from typing import Dict, List + +from moviebot.database.database import DataBase + + +class UserModel: + def __init__(self, user_id: str, movie_choices: Dict[str, str] = None): + """Initializes the user model. + + Args: + user_id: User id. + movie_choices: Dictionary with movie choices (i.e., accept, reject). + Defaults to None. + """ + self.user_id = user_id + self._movies_choices = defaultdict(str) + self._movies_choices.update(movie_choices or {}) + + @property + def movie_choices(self) -> Dict[str, str]: + """Returns user 's movie choices.""" + return self._movies_choices + + def update_movie_choice(self, movie_id: str, choice: str) -> None: + """Updates the choices for a given user. + + Args: + movie_id: Id of the movie. + choice: User choice (i.e., accept, reject). + """ + self._movies_choices[movie_id] = self._movies_choices[movie_id] + [ + choice + ] + + def update_movies_choices(self, movies_choices: Dict[str, str]) -> None: + """Updates the movie choices for a given user. + + Args: + movies_choices: Dictionary with movie choices (i.e., accept, + reject). + """ + self._movies_choices.update(movies_choices) + + def get_movie_choices(self, movie_id: str) -> List[str]: + """Returns the choices for a given movie. + + Args: + movie_id: Id of the movie. + + Returns: + List of previous choices for a movie. + """ + return self._movies_choices[movie_id] + + def _convert_choice_to_preference(self, choice: str) -> float: + """Converts a choice to a preference within the range [-1,1]. + + Dislike is represented by a preference below 0, while like is + represented by a preference above 0. If the choice does not express a + preference (i.e., inquire), then the preference is neutral, i.e., 0. + Possible choices are: accept, reject, dont_like, inquire, and watched. + + Args: + choice: Choice (i.e., accept, reject). + + Returns: + Preference within the range [-1,1]. + """ + if choice == "accept": + return 1.0 + elif choice in ["reject", "dont_like"]: + return -1.0 + + return 0.0 + + def get_tag_preference( + self, slot: str, tag: str, database: DataBase + ) -> str: + """Returns the preference for a given tag (e.g., comedies). + + Args: + slot: Slot name. + tag: Tag. + database: Database with all the movies. + + Returns: + Tag preference. + """ + sql_cursor = database.sql_connection.cursor() + tag_set = sql_cursor.execute( + f"SELECT ID FROM {database._get_table_name()} WHERE {slot} LIKE '%{tag}%'" + ).fetchall() + + preference = 0.0 + count_rated = 0 + for movie_id, choices in self._movies_choices.items(): + if movie_id in tag_set: + # TODO: decide how to handle contradictory choices (e.g., the + # same movie was accepted and rejected) + for choice in choices: + preference += self._convert_choice_to_preference(choice) + count_rated += 1 + + return preference / count_rated if count_rated > 0 else 0.0 From 5411404407290a05ec9ee3da071a949ce0ab6c23 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:25:52 +0200 Subject: [PATCH 02/14] Fix flake issue --- moviebot/user_modeling/user_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 296ea6c..9be43cb 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -92,7 +92,8 @@ def get_tag_preference( """ sql_cursor = database.sql_connection.cursor() tag_set = sql_cursor.execute( - f"SELECT ID FROM {database._get_table_name()} WHERE {slot} LIKE '%{tag}%'" + f"SELECT ID FROM {database._get_table_name()} WHERE {slot} LIKE " + f"'%{tag}%'" ).fetchall() preference = 0.0 From 80e66cc0ee06f585a4e6f7b8f1a9940a0ef37cdf Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:53:50 +0200 Subject: [PATCH 03/14] Update initialisation of UserModel --- moviebot/user_modeling/user_model.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 9be43cb..5566e4d 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -1,6 +1,8 @@ """Class for user modeling.""" +import json +import os from collections import defaultdict from typing import Dict, List @@ -8,17 +10,26 @@ class UserModel: - def __init__(self, user_id: str, movie_choices: Dict[str, str] = None): + def __init__(self, user_id: str, historical_movie_choices_path: str = None): """Initializes the user model. + The JSON file with historical movie choices is a dictionary with the + movie id as key and the list of choices as value. + Args: user_id: User id. - movie_choices: Dictionary with movie choices (i.e., accept, reject). - Defaults to None. + historical_movie_choices_path: Path to the JSON file with historical + movie choices. Defaults to None. """ self.user_id = user_id self._movies_choices = defaultdict(str) - self._movies_choices.update(movie_choices or {}) + + if historical_movie_choices_path and os.path.exists( + historical_movie_choices_path + ): + # Load historical movie choices + movie_choices = json.load(historical_movie_choices_path) + self._movies_choices.update(movie_choices) @property def movie_choices(self) -> Dict[str, str]: From de0f06ba64d58bd299dfacc0a211fde2f8be7a8b Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:34:59 +0200 Subject: [PATCH 04/14] Add method to store explicitly set preferences --- moviebot/user_modeling/user_model.py | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 5566e4d..8539a6d 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -31,6 +31,8 @@ def __init__(self, user_id: str, historical_movie_choices_path: str = None): movie_choices = json.load(historical_movie_choices_path) self._movies_choices.update(movie_choices) + self.tag_preferences = defaultdict(defaultdict(float)) + @property def movie_choices(self) -> Dict[str, str]: """Returns user 's movie choices.""" @@ -88,10 +90,10 @@ def _convert_choice_to_preference(self, choice: str) -> float: return 0.0 - def get_tag_preference( + def compute_tag_preference( self, slot: str, tag: str, database: DataBase ) -> str: - """Returns the preference for a given tag (e.g., comedies). + """Computes the preference for a given tag (e.g., comedies). Args: slot: Slot name. @@ -118,3 +120,33 @@ def get_tag_preference( count_rated += 1 return preference / count_rated if count_rated > 0 else 0.0 + + def get_tag_preference(self, slot: str, tag: str) -> float: + """Returns the preference for a given tag (e.g., comedies). + + If the preference is not explicitly set, then it is computed based on + movies choices. + + Args: + slot: Slot name. + tag: Tag. + + Returns: + Preference. + """ + preference = self.tag_preferences[slot].get(tag, None) + if preference is None: + return self.compute_tag_preference(slot, tag) + return preference + + def set_tag_preference( + self, slot: str, tag: str, preference: float + ) -> None: + """Sets the preference for a given tag (e.g., comedies). + + Args: + slot: Slot name. + tag: Tag. + preference: Preference. + """ + self.tag_preferences[slot][tag] = preference From 0b1ef369c115ed380fe7f29159170fbb1ef8a8c3 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:50:37 +0200 Subject: [PATCH 05/14] Address review comments --- moviebot/user_modeling/user_model.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 8539a6d..d23a79b 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -22,16 +22,16 @@ def __init__(self, user_id: str, historical_movie_choices_path: str = None): movie choices. Defaults to None. """ self.user_id = user_id - self._movies_choices = defaultdict(str) + self._movies_choices = defaultdict(list) if historical_movie_choices_path and os.path.exists( historical_movie_choices_path ): # Load historical movie choices - movie_choices = json.load(historical_movie_choices_path) + movie_choices = json.load(open(historical_movie_choices_path, "r")) self._movies_choices.update(movie_choices) - self.tag_preferences = defaultdict(defaultdict(float)) + self.tag_preferences = defaultdict(lambda: defaultdict(float)) @property def movie_choices(self) -> Dict[str, str]: @@ -45,9 +45,7 @@ def update_movie_choice(self, movie_id: str, choice: str) -> None: movie_id: Id of the movie. choice: User choice (i.e., accept, reject). """ - self._movies_choices[movie_id] = self._movies_choices[movie_id] + [ - choice - ] + self._movies_choices[movie_id].append(choice) def update_movies_choices(self, movies_choices: Dict[str, str]) -> None: """Updates the movie choices for a given user. From 4f11fc795764e17d27dc4ff78400ef01010aca47 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:48:19 +0200 Subject: [PATCH 06/14] Refactor user model --- moviebot/user_modeling/user_model.py | 252 ++++++++++++++++----------- 1 file changed, 153 insertions(+), 99 deletions(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index d23a79b..4b5b965 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -1,71 +1,81 @@ -"""Class for user modeling.""" +"""Class for user modeling. +The user model stores the user's preferences, in terms of slots and items in a +structured (dictionary) and unstructured manner (utterances). These preferences +serves to main purposes: +1. Input for the recommender system. +2. Input for the explainability component to generate an explainable user model. +""" + +from __future__ import annotations import json +import logging import os from collections import defaultdict -from typing import Dict, List +from typing import Dict, List, Optional, Union -from moviebot.database.database import DataBase +from dialoguekit.core import AnnotatedUtterance +from dialoguekit.utils.dialogue_reader import json_to_annotated_utterance class UserModel: - def __init__(self, user_id: str, historical_movie_choices_path: str = None): - """Initializes the user model. - - The JSON file with historical movie choices is a dictionary with the - movie id as key and the list of choices as value. - - Args: - user_id: User id. - historical_movie_choices_path: Path to the JSON file with historical - movie choices. Defaults to None. - """ - self.user_id = user_id - self._movies_choices = defaultdict(list) - - if historical_movie_choices_path and os.path.exists( - historical_movie_choices_path - ): - # Load historical movie choices - movie_choices = json.load(open(historical_movie_choices_path, "r")) - self._movies_choices.update(movie_choices) - - self.tag_preferences = defaultdict(lambda: defaultdict(float)) - - @property - def movie_choices(self) -> Dict[str, str]: - """Returns user 's movie choices.""" - return self._movies_choices - - def update_movie_choice(self, movie_id: str, choice: str) -> None: - """Updates the choices for a given user. + def __init__(self) -> None: + """Initializes the user model.""" + # Structured and unstructured slot preferences + self.slot_preferences: Dict[Dict[str, float]] = defaultdict( + lambda: defaultdict(float) + ) + self.slot_preferences_nl: Dict[ + Dict[str, AnnotatedUtterance] + ] = defaultdict(lambda: defaultdict(list)) + + # Structured and unstructured item preferences + self.item_preferences: Dict[Dict[str, float]] = defaultdict( + lambda: defaultdict(float) + ) + self.item_preferences_nl: Dict[ + Dict[str, AnnotatedUtterance] + ] = defaultdict(lambda: defaultdict(list)) + + @classmethod + def from_json(cls, json_path: str) -> UserModel: + """Loads a user model from a JSON file. Args: - movie_id: Id of the movie. - choice: User choice (i.e., accept, reject). - """ - self._movies_choices[movie_id].append(choice) + json_path: Path to the JSON file. - def update_movies_choices(self, movies_choices: Dict[str, str]) -> None: - """Updates the movie choices for a given user. - - Args: - movies_choices: Dictionary with movie choices (i.e., accept, - reject). - """ - self._movies_choices.update(movies_choices) - - def get_movie_choices(self, movie_id: str) -> List[str]: - """Returns the choices for a given movie. - - Args: - movie_id: Id of the movie. + Raises: + FileNotFoundError: If the JSON file is not found. Returns: - List of previous choices for a movie. + User model. """ - return self._movies_choices[movie_id] + user_model = cls() + if os.path.exists(json_path): + user_model_json = json.load(open(json_path, "r")) + user_model.slot_preferences.update( + user_model_json["slot_preferences"] + ) + for slot, utterance in user_model_json[ + "slot_preferences_nl" + ].items(): + user_model.slot_preferences_nl[slot].append( + json_to_annotated_utterance(utterance) + ) + + user_model.item_preferences.update( + user_model_json["item_preferences"] + ) + for item, utterance in user_model_json[ + "item_preferences_nl" + ].items(): + user_model.item_preferences_nl[item].append( + json_to_annotated_utterance(utterance) + ) + else: + raise FileNotFoundError(f"JSON file {json_path} not found.") + return user_model def _convert_choice_to_preference(self, choice: str) -> float: """Converts a choice to a preference within the range [-1,1]. @@ -88,63 +98,107 @@ def _convert_choice_to_preference(self, choice: str) -> float: return 0.0 - def compute_tag_preference( - self, slot: str, tag: str, database: DataBase - ) -> str: - """Computes the preference for a given tag (e.g., comedies). + def update_item_preference(self, item: str, choice: str) -> None: + """Updates the preference for a given item. Args: - slot: Slot name. - tag: Tag. - database: Database with all the movies. + item: Item. + choice: Choice (i.e., accept, reject, don't like). + """ + self.item_preferences[item][ + choice + ] = self._convert_choice_to_preference(choice) + + def get_utterances_with_item_preferences( + self, item: Optional[str] = None + ) -> List[AnnotatedUtterance]: + """Returns the utterances with item preference. + + If no item is provided, then all the utterances with item preference + are returned. Else, only the utterances with item preference for the + given item are returned. + + Args: + item: Item. Defaults to None. + + Returns: + Utterances with item preference. + """ + if item is None: + return [ + utterance + for utterances in self.item_preferences_nl.values() + for utterance in utterances + ] + + if item not in self.item_preferences_nl: + logging.warning(f"Item {item} not found in user model.") + return self.item_preferences_nl.get(item, []) + + def get_item_preferences( + self, item: Optional[str] = None + ) -> Union[Dict[str, float], float]: + """Returns the item preferences. + + If no item is provided, then all the item preferences are returned. + Else, only the item preferences for the given item are returned. + + Args: + item: Item. Defaults to None. Returns: - Tag preference. + Item preferences. """ - sql_cursor = database.sql_connection.cursor() - tag_set = sql_cursor.execute( - f"SELECT ID FROM {database._get_table_name()} WHERE {slot} LIKE " - f"'%{tag}%'" - ).fetchall() - - preference = 0.0 - count_rated = 0 - for movie_id, choices in self._movies_choices.items(): - if movie_id in tag_set: - # TODO: decide how to handle contradictory choices (e.g., the - # same movie was accepted and rejected) - for choice in choices: - preference += self._convert_choice_to_preference(choice) - count_rated += 1 - - return preference / count_rated if count_rated > 0 else 0.0 - - def get_tag_preference(self, slot: str, tag: str) -> float: - """Returns the preference for a given tag (e.g., comedies). - - If the preference is not explicitly set, then it is computed based on - movies choices. + if item is None: + return self.item_preferences + + if item not in self.item_preferences: + logging.warning(f"Item {item} not found in user model.") + return self.item_preferences.get(item, None) + + def get_utterances_with_slot_preferences( + self, slot: Optional[str] = None + ) -> List[AnnotatedUtterance]: + """Returns the utterances with slot preference. + + If no slot is provided, then all the utterances with slot preference + are returned. Else, only the utterances with slot preference for the + given slot are returned. Args: - slot: Slot name. - tag: Tag. + slot: Slot. Defaults to None. Returns: - Preference. + Utterances with slot preference. """ - preference = self.tag_preferences[slot].get(tag, None) - if preference is None: - return self.compute_tag_preference(slot, tag) - return preference + if slot is None: + return [ + utterance + for utterances in self.slot_preferences_nl.values() + for utterance in utterances + ] + + if slot not in self.slot_preferences_nl: + logging.warning(f"Slot {slot} not found in user model.") + return self.slot_preferences_nl.get(slot, []) - def set_tag_preference( - self, slot: str, tag: str, preference: float - ) -> None: - """Sets the preference for a given tag (e.g., comedies). + def get_slot_preferences( + self, slot: Optional[str] = None + ) -> Union[Dict[str, float], float]: + """Returns the slot preferences. + + If no slot is provided, then all the slot preferences are returned. + Else, only the slot preferences for the given slot are returned. Args: - slot: Slot name. - tag: Tag. - preference: Preference. + slot: Slot. Defaults to None. + + Returns: + Slot preferences. """ - self.tag_preferences[slot][tag] = preference + if slot is None: + return self.slot_preferences + + if slot not in self.slot_preferences: + logging.warning(f"Slot {slot} not found in user model.") + return self.slot_preferences.get(slot, None) From 1b6ea9a0dafdb125c6fdf66a5321211e268fada1 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:04:12 +0200 Subject: [PATCH 07/14] Add a method to save UserModel --- moviebot/user_modeling/user_model.py | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 4b5b965..ea67df3 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -77,6 +77,62 @@ def from_json(cls, json_path: str) -> UserModel: raise FileNotFoundError(f"JSON file {json_path} not found.") return user_model + def _utterance_to_dict( + self, utterance: AnnotatedUtterance + ) -> Dict[str, str]: + """Converts an utterance to a dictionary. + + TODO: Move this method to DialgueKit AnnotatedUtterance class. + + Args: + utterance: Utterance. + + Returns: + Dictionary with utterance information. + """ + return { + "participant": utterance.participant.name, + "utterance": utterance.text, + "intent": utterance.intent.label, + "slot_values": [ + [annotation.slot, annotation.value] + for annotation in utterance.annotations + ] + if utterance.annotations + else [], + } + + def save(self, json_path: str) -> None: + """Saves the user model to a JSON file. + + Args: + json_path: Path to the JSON file. + """ + data = { + "slot_preferences": self.slot_preferences, + "item_preferences": self.item_preferences, + } + + slot_preferences_utterances = {} + for slot, utterances in self.slot_preferences_nl.items(): + slot_preferences_utterances[slot] = [ + self._utterance_to_dict(utterance) for utterance in utterances + ] + + item_preferences_utterances = {} + for item, utterances in self.item_preferences_nl.items(): + item_preferences_utterances[item] = [ + self._utterance_to_dict(utterance) for utterance in utterances + ] + + data.update( + { + "slot_preferences_nl": slot_preferences_utterances, + "item_preferences_nl": item_preferences_utterances, + } + ) + json.dump(data, open(json_path, "w"), indent=4) + def _convert_choice_to_preference(self, choice: str) -> float: """Converts a choice to a preference within the range [-1,1]. From ee695c9a5c0395ea6d1ef8d519d785c0f6e729ec Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:04:52 +0200 Subject: [PATCH 08/14] Fix typo --- moviebot/user_modeling/user_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index ea67df3..4442b73 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -82,7 +82,7 @@ def _utterance_to_dict( ) -> Dict[str, str]: """Converts an utterance to a dictionary. - TODO: Move this method to DialgueKit AnnotatedUtterance class. + TODO: Move this method to DialogueKit AnnotatedUtterance class. Args: utterance: Utterance. From 72fef3efe31a0a3e454adcc2f6bcc17ddaac2ddb Mon Sep 17 00:00:00 2001 From: Nolwenn Bernard <28621493+NoB0@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:18:38 +0200 Subject: [PATCH 09/14] Update moviebot/user_modeling/user_model.py Co-authored-by: Ivica Kostric --- moviebot/user_modeling/user_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 4442b73..18007ce 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -2,7 +2,7 @@ The user model stores the user's preferences, in terms of slots and items in a structured (dictionary) and unstructured manner (utterances). These preferences -serves to main purposes: +serve two main purposes: 1. Input for the recommender system. 2. Input for the explainability component to generate an explainable user model. """ From c09866d6150998e8650455c4c4afede6c9cebe7a Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:24:01 +0200 Subject: [PATCH 10/14] Address review comments --- moviebot/nlg/nlg.py | 53 +++++++-- .../nlu/recommendation_decision_processing.py | 38 +++++++ moviebot/nlu/user_intents_checker.py | 20 +++- moviebot/user_modeling/user_model.py | 102 ++++++++---------- 4 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 moviebot/nlu/recommendation_decision_processing.py diff --git a/moviebot/nlg/nlg.py b/moviebot/nlg/nlg.py index a1128ee..4933450 100644 --- a/moviebot/nlg/nlg.py +++ b/moviebot/nlg/nlg.py @@ -14,6 +14,10 @@ from moviebot.nlu.annotation.operator import Operator from moviebot.nlu.annotation.slots import Slots from moviebot.nlu.annotation.values import Values +from moviebot.nlu.user_intents_checker import ( + RecommendationChoices, + convert_choice_to_preference, +) ButtonOptions = Dict[DialogueAct, List[str]] CINType = Dict[str, Union[str, List[str]]] @@ -141,7 +145,9 @@ def __init__(self, args=None) -> None: self.slot_not_found = { Slots.GENRES.value: ["I could not find the genres __replace__."], - Slots.KEYWORDS.value: ["I couldn't find the keywords __replace__."], + Slots.KEYWORDS.value: [ + "I couldn't find the keywords __replace__." + ], Slots.DIRECTORS.value: [ "I could not find the the director name __replace__." ], @@ -514,7 +520,9 @@ def _clarify_CIN( # noqa: C901 ) else: response = ( - response + " named similar to " + CIN[Slots.TITLE.value] + response + + " named similar to " + + CIN[Slots.TITLE.value] ) if ( CIN[Slots.DIRECTORS.value] @@ -572,7 +580,9 @@ def _user_options_continue(self, agent_dact: DialogueAct) -> ButtonOptions: DialogueAct(UserIntents.RESTART, []): [ "I want to restart for a new movie." ], - DialogueAct(UserIntents.BYE, []): ["I would like to quit now."], + DialogueAct(UserIntents.BYE, []): [ + "I would like to quit now." + ], } return options @@ -585,19 +595,46 @@ def _user_options_recommend(self) -> ButtonOptions: options = { DialogueAct( UserIntents.REJECT, - [ItemConstraint("reason", Operator.EQ, "watched")], + [ + ItemConstraint("reason", Operator.EQ, "watched"), + ItemConstraint( + "preference", + Operator.EQ, + convert_choice_to_preference( + RecommendationChoices.WATCHED + ), + ), + ], ): ["I have already watched it."], # [random.choice(['I have already watched it.', # 'I have seen this already.'])], DialogueAct( UserIntents.REJECT, - [ItemConstraint("reason", Operator.EQ, "dont_like")], + [ + ItemConstraint("reason", Operator.EQ, "dont_like"), + ItemConstraint( + "preference", + Operator.EQ, + convert_choice_to_preference( + RecommendationChoices.DONT_LIKE + ), + ), + ], ): ["Recommend me something else please."], # [random.choice(['I don\'t like this recommendation.', # 'Recommend me something else please.'])], - DialogueAct(UserIntents.ACCEPT, []): [ - "I like this recommendation." - ], + DialogueAct( + UserIntents.ACCEPT, + [ + ItemConstraint( + "preference", + Operator.EQ, + convert_choice_to_preference( + RecommendationChoices.ACCEPT + ), + ), + ], + ): ["I like this recommendation."], DialogueAct( UserIntents.INQUIRE, [ItemConstraint(Slots.MORE_INFO.value, Operator.EQ, "")], diff --git a/moviebot/nlu/recommendation_decision_processing.py b/moviebot/nlu/recommendation_decision_processing.py new file mode 100644 index 0000000..9e14a25 --- /dev/null +++ b/moviebot/nlu/recommendation_decision_processing.py @@ -0,0 +1,38 @@ +"""This module is used to process the user's decision on a movie recommendation. +""" + +from enum import Enum + + +class RecommendationChoices(Enum): + """Enum class for recommendation choices.""" + + ACCEPT = "accept" + REJECT = "reject" + DONT_LIKE = "dont_like" + WATCHED = "watched" + + +def convert_choice_to_preference(choice: RecommendationChoices) -> float: + """Converts a choice to a preference within the range [-1,1]. + + Dislike is represented by a preference below 0, while like is + represented by a preference above 0. If the choice does not express a + preference (e.g., inquire), then the preference is neutral, i.e., 0. + Possible choices are: accept, reject, dont_like, inquire, and watched. + + Args: + choice: Choice. + + Returns: + Preference within the range [-1,1]. + """ + if choice == RecommendationChoices.ACCEPT: + return 1.0 + elif choice in [ + RecommendationChoices.REJECT, + RecommendationChoices.DONT_LIKE, + ]: + return -1.0 + + return 0.0 diff --git a/moviebot/nlu/user_intents_checker.py b/moviebot/nlu/user_intents_checker.py index bbc561b..9d522f0 100644 --- a/moviebot/nlu/user_intents_checker.py +++ b/moviebot/nlu/user_intents_checker.py @@ -18,6 +18,10 @@ from moviebot.nlu.annotation.slots import Slots from moviebot.nlu.annotation.values import Values from moviebot.nlu.data_loader import DataLoader +from moviebot.nlu.recommendation_decision_processing import ( + RecommendationChoices, + convert_choice_to_preference, +) from moviebot.ontology.ontology import Ontology PATTERN_BASIC = { @@ -334,7 +338,13 @@ def check_reject_intent( ] ): dact.intent = UserIntents.REJECT - dact.params = [ItemConstraint("reason", Operator.EQ, "dont_like")] + preference = convert_choice_to_preference( + RecommendationChoices.DONT_LIKE + ) + dact.params = [ + ItemConstraint("reason", Operator.EQ, "dont_like"), + ItemConstraint("preference", Operator.EQ, preference), + ] elif any( [ re.search(r"\b{0}\b".format(pattern), utterance) @@ -342,7 +352,13 @@ def check_reject_intent( ] ): dact.intent = UserIntents.REJECT - dact.params = [ItemConstraint("reason", Operator.EQ, "watched")] + preference = convert_choice_to_preference( + RecommendationChoices.WATCHED + ) + dact.params = [ + ItemConstraint("reason", Operator.EQ, "watched"), + ItemConstraint("preference", Operator.EQ, preference), + ] if dact.intent != UserIntents.UNK: user_dacts.append(dact) return user_dacts diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 18007ce..d0c10d6 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -18,25 +18,29 @@ from dialoguekit.core import AnnotatedUtterance from dialoguekit.utils.dialogue_reader import json_to_annotated_utterance +_KEY_SLOT_PREFERENCES = "slot_preferences" +_KEY_SLOT_PREFERENCES_NL = "slot_preferences_nl" +_KEY_ITEM_PREFERENCES = "item_preferences" +_KEY_ITEM_PREFERENCES_NL = "item_preferences_nl" + class UserModel: def __init__(self) -> None: """Initializes the user model.""" # Structured and unstructured slot preferences - self.slot_preferences: Dict[Dict[str, float]] = defaultdict( + self.slot_preferences: Dict[str, Dict[str, float]] = defaultdict( lambda: defaultdict(float) ) self.slot_preferences_nl: Dict[ - Dict[str, AnnotatedUtterance] + str, Dict[str, AnnotatedUtterance] ] = defaultdict(lambda: defaultdict(list)) # Structured and unstructured item preferences - self.item_preferences: Dict[Dict[str, float]] = defaultdict( - lambda: defaultdict(float) - ) + self.item_preferences: Dict[str, float] = defaultdict(float) + self.item_preferences_nl: Dict[ - Dict[str, AnnotatedUtterance] - ] = defaultdict(lambda: defaultdict(list)) + str, List[AnnotatedUtterance] + ] = defaultdict(list) @classmethod def from_json(cls, json_path: str) -> UserModel: @@ -52,29 +56,29 @@ def from_json(cls, json_path: str) -> UserModel: User model. """ user_model = cls() - if os.path.exists(json_path): - user_model_json = json.load(open(json_path, "r")) - user_model.slot_preferences.update( - user_model_json["slot_preferences"] + if not os.path.exists(json_path): + raise FileNotFoundError(f"JSON file {json_path} not found.") + + user_model_json = json.load(open(json_path, "r")) + user_model.slot_preferences.update( + user_model_json[_KEY_SLOT_PREFERENCES] + ) + for slot, utterance in user_model_json[ + _KEY_SLOT_PREFERENCES_NL + ].items(): + user_model.slot_preferences_nl[slot].append( + json_to_annotated_utterance(utterance) ) - for slot, utterance in user_model_json[ - "slot_preferences_nl" - ].items(): - user_model.slot_preferences_nl[slot].append( - json_to_annotated_utterance(utterance) - ) - - user_model.item_preferences.update( - user_model_json["item_preferences"] + + user_model.item_preferences.update( + user_model_json[_KEY_ITEM_PREFERENCES] + ) + for item, utterance in user_model_json[ + _KEY_ITEM_PREFERENCES_NL + ].items(): + user_model.item_preferences_nl[item].append( + json_to_annotated_utterance(utterance) ) - for item, utterance in user_model_json[ - "item_preferences_nl" - ].items(): - user_model.item_preferences_nl[item].append( - json_to_annotated_utterance(utterance) - ) - else: - raise FileNotFoundError(f"JSON file {json_path} not found.") return user_model def _utterance_to_dict( @@ -83,6 +87,7 @@ def _utterance_to_dict( """Converts an utterance to a dictionary. TODO: Move this method to DialogueKit AnnotatedUtterance class. + See: https://github.com/iai-group/DialogueKit/issues/248 Args: utterance: Utterance. @@ -102,15 +107,15 @@ def _utterance_to_dict( else [], } - def save(self, json_path: str) -> None: + def save_as_json_file(self, json_path: str) -> None: """Saves the user model to a JSON file. Args: json_path: Path to the JSON file. """ data = { - "slot_preferences": self.slot_preferences, - "item_preferences": self.item_preferences, + _KEY_SLOT_PREFERENCES: self.slot_preferences, + _KEY_ITEM_PREFERENCES: self.item_preferences, } slot_preferences_utterances = {} @@ -127,43 +132,20 @@ def save(self, json_path: str) -> None: data.update( { - "slot_preferences_nl": slot_preferences_utterances, - "item_preferences_nl": item_preferences_utterances, + _KEY_SLOT_PREFERENCES_NL: slot_preferences_utterances, + _KEY_ITEM_PREFERENCES_NL: item_preferences_utterances, } ) json.dump(data, open(json_path, "w"), indent=4) - def _convert_choice_to_preference(self, choice: str) -> float: - """Converts a choice to a preference within the range [-1,1]. - - Dislike is represented by a preference below 0, while like is - represented by a preference above 0. If the choice does not express a - preference (i.e., inquire), then the preference is neutral, i.e., 0. - Possible choices are: accept, reject, dont_like, inquire, and watched. - - Args: - choice: Choice (i.e., accept, reject). - - Returns: - Preference within the range [-1,1]. - """ - if choice == "accept": - return 1.0 - elif choice in ["reject", "dont_like"]: - return -1.0 - - return 0.0 - - def update_item_preference(self, item: str, choice: str) -> None: + def update_item_preference(self, item: str, preference: float) -> None: """Updates the preference for a given item. Args: item: Item. - choice: Choice (i.e., accept, reject, don't like). + preference: Preference. """ - self.item_preferences[item][ - choice - ] = self._convert_choice_to_preference(choice) + self.item_preferences[item] = preference def get_utterances_with_item_preferences( self, item: Optional[str] = None @@ -240,7 +222,7 @@ def get_utterances_with_slot_preferences( def get_slot_preferences( self, slot: Optional[str] = None - ) -> Union[Dict[str, float], float]: + ) -> Dict[str, float]: """Returns the slot preferences. If no slot is provided, then all the slot preferences are returned. From 2fa2bb8f3b4a10441744aa99f336e2b369dd5fa9 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:04:45 +0200 Subject: [PATCH 11/14] Add tests for user model --- moviebot/user_modeling/user_model.py | 24 ++--- tests/nlu/test_user_intents_checker.py | 14 ++- tests/user_modeling/test_user_modeling.py | 112 ++++++++++++++++++++++ 3 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 tests/user_modeling/test_user_modeling.py diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index d0c10d6..c7213ec 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -138,15 +138,6 @@ def save_as_json_file(self, json_path: str) -> None: ) json.dump(data, open(json_path, "w"), indent=4) - def update_item_preference(self, item: str, preference: float) -> None: - """Updates the preference for a given item. - - Args: - item: Item. - preference: Preference. - """ - self.item_preferences[item] = preference - def get_utterances_with_item_preferences( self, item: Optional[str] = None ) -> List[AnnotatedUtterance]: @@ -195,8 +186,8 @@ def get_item_preferences( return self.item_preferences.get(item, None) def get_utterances_with_slot_preferences( - self, slot: Optional[str] = None - ) -> List[AnnotatedUtterance]: + self, slot: Optional[str] = None, value: Optional[str] = None + ) -> Union[Dict[str, List[AnnotatedUtterance]], List[AnnotatedUtterance]]: """Returns the utterances with slot preference. If no slot is provided, then all the utterances with slot preference @@ -205,6 +196,7 @@ def get_utterances_with_slot_preferences( Args: slot: Slot. Defaults to None. + value: Value. Defaults to None. Returns: Utterances with slot preference. @@ -218,11 +210,19 @@ def get_utterances_with_slot_preferences( if slot not in self.slot_preferences_nl: logging.warning(f"Slot {slot} not found in user model.") + + if value is not None: + if value not in self.slot_preferences_nl.get(slot, {}): + logging.warning( + f"Value {value} not found for slot {slot} in user model." + ) + return self.slot_preferences_nl.get(slot, {}).get(value, []) + return self.slot_preferences_nl.get(slot, []) def get_slot_preferences( self, slot: Optional[str] = None - ) -> Dict[str, float]: + ) -> Union[Dict[str, Dict[str, float]], Dict[str, float]]: """Returns the slot preferences. If no slot is provided, then all the slot preferences are returned. diff --git a/tests/nlu/test_user_intents_checker.py b/tests/nlu/test_user_intents_checker.py index 2da5300..f06d93a 100644 --- a/tests/nlu/test_user_intents_checker.py +++ b/tests/nlu/test_user_intents_checker.py @@ -193,7 +193,12 @@ def test_check_reveal_voluntary_intent_empty( "reason", Operator.EQ, "dont_like", - ) + ), + ItemConstraint( + "preference", + Operator.EQ, + -1.0, + ), ], ), ), @@ -206,7 +211,12 @@ def test_check_reveal_voluntary_intent_empty( "reason", Operator.EQ, "watched", - ) + ), + ItemConstraint( + "preference", + Operator.EQ, + 0.0, + ), ], ), ), diff --git a/tests/user_modeling/test_user_modeling.py b/tests/user_modeling/test_user_modeling.py new file mode 100644 index 0000000..aba83be --- /dev/null +++ b/tests/user_modeling/test_user_modeling.py @@ -0,0 +1,112 @@ +"""Tests for user modeling.""" + +import pytest + +from dialoguekit.core.annotated_utterance import AnnotatedUtterance +from dialoguekit.participant.participant import DialogueParticipant +from moviebot.core.intents.user_intents import UserIntents +from moviebot.nlu.recommendation_decision_processing import ( + RecommendationChoices, + convert_choice_to_preference, +) +from moviebot.user_modeling.user_model import UserModel + + +@pytest.fixture +def user_model() -> UserModel: + return UserModel() + + +def test_get_item_preferences(user_model: UserModel) -> None: + """Tests get_item_preference.""" + preference = convert_choice_to_preference(RecommendationChoices.ACCEPT) + user_model.item_preferences["movie1"] = preference + preference = convert_choice_to_preference(RecommendationChoices.REJECT) + user_model.item_preferences["movie2"] = preference + + assert user_model.get_item_preferences("movie1") == 1.0 + assert user_model.get_item_preferences("movie5") is None + assert user_model.get_item_preferences() == {"movie1": 1.0, "movie2": -1.0} + + +def test_get_utterances_with_item_preferences(user_model: UserModel) -> None: + """Tests get_utterances_with_item_preference.""" + user_model.item_preferences_nl["movie1"] = [ + AnnotatedUtterance( + "I like movie1", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + AnnotatedUtterance( + "Sounds good", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + ] + + assert user_model.get_utterances_with_item_preferences("movie1") == [ + AnnotatedUtterance( + "I like movie1", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + AnnotatedUtterance( + "Sounds good", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + ] + assert user_model.get_utterances_with_item_preferences("movie2") == [] + + +def test_get_slot_preferences(user_model: UserModel) -> None: + """Tests get_slot_preference.""" + user_model.slot_preferences["genre"]["comedy"] = 1.0 + user_model.slot_preferences["genre"]["action"] = -1.0 + user_model.slot_preferences["genre"]["drama"] = -1.0 + + assert user_model.get_slot_preferences("genre") == { + "comedy": 1.0, + "action": -1.0, + "drama": -1.0, + } + assert user_model.get_slot_preferences() == { + "genre": {"comedy": 1.0, "action": -1.0, "drama": -1.0} + } + assert user_model.get_slot_preferences("director") is None + + +def test_get_utterances_with_slot_preferences(user_model: UserModel) -> None: + """Tests get_utterances_with_slot_preferences.""" + user_model.slot_preferences_nl["genre"]["action"] = [ + AnnotatedUtterance( + "I don't like action", + DialogueParticipant.USER, + intent=UserIntents.REVEAL.value, + ), + AnnotatedUtterance( + "I don't want to watch an action movie", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + ] + + assert user_model.get_utterances_with_slot_preferences("actors") == [] + assert ( + user_model.get_utterances_with_slot_preferences("genre", "comedy") + == [] + ) + assert user_model.get_utterances_with_slot_preferences( + "genre", "action" + ) == [ + AnnotatedUtterance( + "I don't like action", + DialogueParticipant.USER, + intent=UserIntents.REVEAL.value, + ), + AnnotatedUtterance( + "I don't want to watch an action movie", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + ] From 34f916b9438234c709702cd91c061fd3eed7dfe9 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:00:29 +0200 Subject: [PATCH 12/14] Address review comments --- moviebot/nlu/user_intents_checker.py | 2 ++ moviebot/user_modeling/user_model.py | 38 ++++++++++++-------- tests/user_modeling/test_user_modeling.py | 43 ++++++++++++++++++++++- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/moviebot/nlu/user_intents_checker.py b/moviebot/nlu/user_intents_checker.py index e035fae..cf648dd 100644 --- a/moviebot/nlu/user_intents_checker.py +++ b/moviebot/nlu/user_intents_checker.py @@ -341,6 +341,8 @@ def check_reject_intent( preference = convert_choice_to_preference( RecommendationChoices.DONT_LIKE ) + # TODO: Use enum for constraints' slot. + # See: https://github.com/iai-group/MovieBot/issues/225 dact.params = [ ItemConstraint("reason", Operator.EQ, "dont_like"), ItemConstraint("preference", Operator.EQ, preference), diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index c7213ec..b54d3d6 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -28,6 +28,9 @@ class UserModel: def __init__(self) -> None: """Initializes the user model.""" # Structured and unstructured slot preferences + # For slots, the first key is the slot name, and the second key is the + # slot value, e.g., {"genre": {"drama": 1.0, "action": -.5}}. For + # unstructured, the value is a list of annotated utterances. self.slot_preferences: Dict[str, Dict[str, float]] = defaultdict( lambda: defaultdict(float) ) @@ -36,6 +39,8 @@ def __init__(self) -> None: ] = defaultdict(lambda: defaultdict(list)) # Structured and unstructured item preferences + # The key is the item id and the value is either a number or a list of + # annotated utterances. self.item_preferences: Dict[str, float] = defaultdict(float) self.item_preferences_nl: Dict[ @@ -63,22 +68,25 @@ def from_json(cls, json_path: str) -> UserModel: user_model.slot_preferences.update( user_model_json[_KEY_SLOT_PREFERENCES] ) - for slot, utterance in user_model_json[ + for slot, value_utterances in user_model_json[ _KEY_SLOT_PREFERENCES_NL ].items(): - user_model.slot_preferences_nl[slot].append( - json_to_annotated_utterance(utterance) - ) + for value, utterances in value_utterances.items(): + for utterance in utterances: + user_model.slot_preferences_nl[slot][value].append( + json_to_annotated_utterance(utterance) + ) user_model.item_preferences.update( user_model_json[_KEY_ITEM_PREFERENCES] ) - for item, utterance in user_model_json[ + for item, utterances in user_model_json[ _KEY_ITEM_PREFERENCES_NL ].items(): - user_model.item_preferences_nl[item].append( - json_to_annotated_utterance(utterance) - ) + for utterance in utterances: + user_model.item_preferences_nl[item].append( + json_to_annotated_utterance(utterance) + ) return user_model def _utterance_to_dict( @@ -118,13 +126,15 @@ def save_as_json_file(self, json_path: str) -> None: _KEY_ITEM_PREFERENCES: self.item_preferences, } - slot_preferences_utterances = {} - for slot, utterances in self.slot_preferences_nl.items(): - slot_preferences_utterances[slot] = [ - self._utterance_to_dict(utterance) for utterance in utterances - ] + slot_preferences_utterances = defaultdict(lambda: defaultdict(list)) + for slot, value_utterances in self.slot_preferences_nl.items(): + for value, utterances in value_utterances.items(): + slot_preferences_utterances[slot][value] = [ + self._utterance_to_dict(utterance) + for utterance in utterances + ] - item_preferences_utterances = {} + item_preferences_utterances = defaultdict(list) for item, utterances in self.item_preferences_nl.items(): item_preferences_utterances[item] = [ self._utterance_to_dict(utterance) for utterance in utterances diff --git a/tests/user_modeling/test_user_modeling.py b/tests/user_modeling/test_user_modeling.py index aba83be..f596594 100644 --- a/tests/user_modeling/test_user_modeling.py +++ b/tests/user_modeling/test_user_modeling.py @@ -1,5 +1,7 @@ """Tests for user modeling.""" +import os + import pytest from dialoguekit.core.annotated_utterance import AnnotatedUtterance @@ -12,11 +14,16 @@ from moviebot.user_modeling.user_model import UserModel -@pytest.fixture +@pytest.fixture(scope="session") def user_model() -> UserModel: return UserModel() +@pytest.fixture +def filepath() -> str: + return "tests/data/test_user_model.json" + + def test_get_item_preferences(user_model: UserModel) -> None: """Tests get_item_preference.""" preference = convert_choice_to_preference(RecommendationChoices.ACCEPT) @@ -110,3 +117,37 @@ def test_get_utterances_with_slot_preferences(user_model: UserModel) -> None: intent=UserIntents.ACCEPT.value, ), ] + + +def test_save_as_json_file(user_model: UserModel, filepath: str) -> None: + """Tests save_as_json_file.""" + user_model.save_as_json_file(filepath) + + assert os.path.exists(filepath) + + +def test_user_model_from_json(filepath: str) -> None: + """Tests class method from_json.""" + user_model = UserModel.from_json(filepath) + + assert user_model.get_item_preferences() == { + "movie1": 1.0, + "movie2": -1.0, + } + + assert user_model.get_utterances_with_slot_preferences() == { + "genre": { + "action": [ + AnnotatedUtterance( + "I don't like action", + DialogueParticipant.USER, + intent=UserIntents.REVEAL.value, + ), + AnnotatedUtterance( + "I don't want to watch an action movie", + DialogueParticipant.USER, + intent=UserIntents.ACCEPT.value, + ), + ] + } + } From c406c888f0ae7d17f7c117d0c8d0c55066ecbf9f Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:11:44 +0200 Subject: [PATCH 13/14] Fix get_utterances_with_slot_preferences --- moviebot/user_modeling/user_model.py | 6 +-- tests/data/test_user_model.json | 47 +++++++++++++++++++++++ tests/user_modeling/test_user_modeling.py | 4 +- 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 tests/data/test_user_model.json diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index b54d3d6..1d29cec 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -212,11 +212,7 @@ def get_utterances_with_slot_preferences( Utterances with slot preference. """ if slot is None: - return [ - utterance - for utterances in self.slot_preferences_nl.values() - for utterance in utterances - ] + return self.slot_preferences_nl if slot not in self.slot_preferences_nl: logging.warning(f"Slot {slot} not found in user model.") diff --git a/tests/data/test_user_model.json b/tests/data/test_user_model.json new file mode 100644 index 0000000..9b80aee --- /dev/null +++ b/tests/data/test_user_model.json @@ -0,0 +1,47 @@ +{ + "slot_preferences": { + "genre": { + "comedy": 1.0, + "action": -1.0, + "drama": -1.0 + } + }, + "item_preferences": { + "movie1": 1.0, + "movie2": -1.0 + }, + "slot_preferences_nl": { + "genre": { + "action": [ + { + "participant": "USER", + "utterance": "I don't like action", + "intent": "reveal", + "slot_values": [] + }, + { + "participant": "USER", + "utterance": "I don't want to watch an action movie", + "intent": "accept", + "slot_values": [] + } + ] + } + }, + "item_preferences_nl": { + "movie1": [ + { + "participant": "USER", + "utterance": "I like movie1", + "intent": "accept", + "slot_values": [] + }, + { + "participant": "USER", + "utterance": "Sounds good", + "intent": "accept", + "slot_values": [] + } + ] + } +} \ No newline at end of file diff --git a/tests/user_modeling/test_user_modeling.py b/tests/user_modeling/test_user_modeling.py index f596594..e3bbeb7 100644 --- a/tests/user_modeling/test_user_modeling.py +++ b/tests/user_modeling/test_user_modeling.py @@ -140,12 +140,12 @@ def test_user_model_from_json(filepath: str) -> None: "action": [ AnnotatedUtterance( "I don't like action", - DialogueParticipant.USER, + DialogueParticipant.USER.name, intent=UserIntents.REVEAL.value, ), AnnotatedUtterance( "I don't want to watch an action movie", - DialogueParticipant.USER, + DialogueParticipant.USER.name, intent=UserIntents.ACCEPT.value, ), ] From 16940b22d2ebb6d4bcc61165cf198b53019207d9 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:12:43 +0200 Subject: [PATCH 14/14] Black --- moviebot/nlg/nlg.py | 12 +++--------- tests/user_modeling/test_user_modeling.py | 3 +-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/moviebot/nlg/nlg.py b/moviebot/nlg/nlg.py index 4933450..1625875 100644 --- a/moviebot/nlg/nlg.py +++ b/moviebot/nlg/nlg.py @@ -145,9 +145,7 @@ def __init__(self, args=None) -> None: self.slot_not_found = { Slots.GENRES.value: ["I could not find the genres __replace__."], - Slots.KEYWORDS.value: [ - "I couldn't find the keywords __replace__." - ], + Slots.KEYWORDS.value: ["I couldn't find the keywords __replace__."], Slots.DIRECTORS.value: [ "I could not find the the director name __replace__." ], @@ -520,9 +518,7 @@ def _clarify_CIN( # noqa: C901 ) else: response = ( - response - + " named similar to " - + CIN[Slots.TITLE.value] + response + " named similar to " + CIN[Slots.TITLE.value] ) if ( CIN[Slots.DIRECTORS.value] @@ -580,9 +576,7 @@ def _user_options_continue(self, agent_dact: DialogueAct) -> ButtonOptions: DialogueAct(UserIntents.RESTART, []): [ "I want to restart for a new movie." ], - DialogueAct(UserIntents.BYE, []): [ - "I would like to quit now." - ], + DialogueAct(UserIntents.BYE, []): ["I would like to quit now."], } return options diff --git a/tests/user_modeling/test_user_modeling.py b/tests/user_modeling/test_user_modeling.py index e3bbeb7..1127019 100644 --- a/tests/user_modeling/test_user_modeling.py +++ b/tests/user_modeling/test_user_modeling.py @@ -100,8 +100,7 @@ def test_get_utterances_with_slot_preferences(user_model: UserModel) -> None: assert user_model.get_utterances_with_slot_preferences("actors") == [] assert ( - user_model.get_utterances_with_slot_preferences("genre", "comedy") - == [] + user_model.get_utterances_with_slot_preferences("genre", "comedy") == [] ) assert user_model.get_utterances_with_slot_preferences( "genre", "action"