diff --git a/moviebot/controller/http_data_formatter.py b/moviebot/controller/http_data_formatter.py index 9a5fd94..bb7255a 100644 --- a/moviebot/controller/http_data_formatter.py +++ b/moviebot/controller/http_data_formatter.py @@ -1,10 +1,12 @@ """This file contains methods to format data for HTTP requests.""" + from __future__ import annotations from dataclasses import asdict, dataclass, field from typing import Any, Dict, List, Tuple from dialoguekit.core import AnnotatedUtterance, Utterance + from moviebot.core.core_types import DialogueOptions HTTP_OBJECT_MESSAGE = Dict[str, Dict[str, str]] @@ -71,6 +73,12 @@ def from_utterance(self, utterance: Utterance) -> Message: utterance.metadata.get("recommended_item") ) message.attachments.append(movie_attachments) + if "explanation" in utterance.metadata: + message.attachments.append( + get_explanation_attachment( + utterance.metadata.get("explanation") + ) + ) return message @@ -110,6 +118,28 @@ def get_buttons_attachment(user_options: DialogueOptions) -> Attachment: return Attachment(type="buttons", payload={"buttons": options}) +def get_explanation_attachment( + explanation_utterance: AnnotatedUtterance, +) -> Attachment: + """Creates an explanation attachment. + + Args: + explanation_utterance: Utterance containing explanation as natural + language and raw key-value pairs stored as annotations. + + Returns: + Explanation attachment. + """ + raw_user_model = "\n".join( + f"{annotation.key}:\t{annotation.value}" + for annotation in explanation_utterance.annotations + ) + return Attachment( + type="explanation", + payload={"text": explanation_utterance.text, "raw": raw_user_model}, + ) + + def get_movie_message_data(info: Dict[str, Any]) -> Tuple[str, Attachment]: """Creates formatted message with movie information and movie image attachment. diff --git a/moviebot/explainability/explainable_user_model.py b/moviebot/explainability/explainable_user_model.py index 3fee96a..acf6cd7 100644 --- a/moviebot/explainability/explainable_user_model.py +++ b/moviebot/explainability/explainable_user_model.py @@ -1,24 +1,44 @@ """Abstract class for creating explainable user models.""" from abc import ABC, abstractmethod -from typing import Dict +from typing import Dict, List -from dialoguekit.core import AnnotatedUtterance +from dialoguekit.core import AnnotatedUtterance, Annotation + +from moviebot.user_modeling.user_model import UserModel UserPreferences = Dict[str, Dict[str, float]] class ExplainableUserModel(ABC): + def __init__(self, user_model: UserModel) -> None: + self._user_model = user_model + + def get_preferences_as_annotations(self) -> List[Annotation]: + """Returns the key-value pairs of the user model. + + Returns: + Key-value pairs of the user model. + """ + all_preferences = self._user_model.get_all_slot_preferences() + return [ + Annotation(key, value) for key, value in all_preferences.items() + ] + + def generate_explanation(self) -> AnnotatedUtterance: + """Generates an explanation based on the user model. + + Returns: + A system utterance containing an explanation. + """ + explanation = self._generate_explanation() + explanation.annotations = self.get_preferences_as_annotations() + return explanation + @abstractmethod - def generate_explanation( - self, user_preferences: UserPreferences - ) -> AnnotatedUtterance: + def _generate_explanation(self) -> AnnotatedUtterance: """Generates an explanation based on the provided input data. - Args: - input_data: The input data for which an explanation is to be - generated. - Returns: A system utterance containing an explanation. diff --git a/moviebot/explainability/explainable_user_model_tag_based.py b/moviebot/explainability/explainable_user_model_tag_based.py index 4a5929d..100478d 100644 --- a/moviebot/explainability/explainable_user_model_tag_based.py +++ b/moviebot/explainability/explainable_user_model_tag_based.py @@ -13,28 +13,29 @@ import re import yaml - from dialoguekit.core import AnnotatedUtterance from dialoguekit.participant import DialogueParticipant + from moviebot.explainability.explainable_user_model import ( ExplainableUserModel, UserPreferences, ) +from moviebot.user_modeling.user_model import UserModel _DEFAULT_TEMPLATE_FILE = "data/explainability/explanation_templates.yaml" class ExplainableUserModelTagBased(ExplainableUserModel): - def __init__(self, template_file: str = _DEFAULT_TEMPLATE_FILE): + def __init__( + self, user_model: UserModel, template_file: str = _DEFAULT_TEMPLATE_FILE + ): """Initializes the ExplainableUserModelTagBased class. Args: template_file: Path to the YAML file containing explanation templates. Defaults to _DEFAULT_TEMPLATE_FILE. - - Raises: - FileNotFoundError: The template file could not be found. """ + super().__init__(user_model) if not os.path.isfile(template_file): raise FileNotFoundError( f"Could not find template file {template_file}." @@ -43,7 +44,7 @@ def __init__(self, template_file: str = _DEFAULT_TEMPLATE_FILE): with open(template_file, "r") as f: self.templates = yaml.safe_load(f) - def generate_explanation( + def _generate_explanation( self, user_preferences: UserPreferences ) -> AnnotatedUtterance: """Generates an explanation based on the provided user preferences. diff --git a/moviebot/user_modeling/user_model.py b/moviebot/user_modeling/user_model.py index 1d29cec..c49c968 100644 --- a/moviebot/user_modeling/user_model.py +++ b/moviebot/user_modeling/user_model.py @@ -34,18 +34,18 @@ def __init__(self) -> None: self.slot_preferences: Dict[str, Dict[str, float]] = defaultdict( lambda: defaultdict(float) ) - self.slot_preferences_nl: Dict[ - str, Dict[str, AnnotatedUtterance] - ] = defaultdict(lambda: defaultdict(list)) + self.slot_preferences_nl: Dict[str, Dict[str, AnnotatedUtterance]] = ( + 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[ - str, List[AnnotatedUtterance] - ] = defaultdict(list) + self.item_preferences_nl: Dict[str, List[AnnotatedUtterance]] = ( + defaultdict(list) + ) @classmethod def from_json(cls, json_path: str) -> UserModel: @@ -107,12 +107,14 @@ def _utterance_to_dict( "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 [], + "slot_values": ( + [ + [annotation.slot, annotation.value] + for annotation in utterance.annotations + ] + if utterance.annotations + else [] + ), } def save_as_json_file(self, json_path: str) -> None: @@ -246,3 +248,11 @@ def get_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) + + def get_all_slot_preferences(self) -> Dict[str, float]: + """Returns all slot preferences as a flat dictionary.""" + return { + key: value + for _, prefs in self.slot_preferences.items() + for key, value in prefs.items() + }