Skip to content

Commit

Permalink
Add ExplainableUserModel Class
Browse files Browse the repository at this point in the history
Fixes #222
  • Loading branch information
IKostric committed Feb 26, 2024
1 parent bcdb7a1 commit db46912
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 27 deletions.
30 changes: 30 additions & 0 deletions moviebot/controller/http_data_formatter.py
Original file line number Diff line number Diff line change
@@ -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]]
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down
38 changes: 29 additions & 9 deletions moviebot/explainability/explainable_user_model.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
13 changes: 7 additions & 6 deletions moviebot/explainability/explainable_user_model_tag_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
Expand All @@ -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.
Expand Down
34 changes: 22 additions & 12 deletions moviebot/user_modeling/user_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
}

0 comments on commit db46912

Please sign in to comment.