495 lines
16 KiB
Plaintext
495 lines
16 KiB
Plaintext
|
{
|
||
|
"cells": [
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "90c05009",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Zarządzanie dialogiem z wykorzystaniem reguł\n",
|
||
|
"============================================\n",
|
||
|
"\n",
|
||
|
"Agent dialogowy wykorzystuje do zarządzanie dialogiem dwa moduły:\n",
|
||
|
"\n",
|
||
|
" - monitor stanu dialogu (dialogue state tracker, DST) — moduł odpowiedzialny za śledzenie stanu dialogu.\n",
|
||
|
"\n",
|
||
|
" - taktykę prowadzenia dialogu (dialogue policy) — moduł, który na podstawie stanu dialogu\n",
|
||
|
" podejmuje decyzję o tym jaką akcję (akt systemu) agent ma podjąć w kolejnej turze.\n",
|
||
|
"\n",
|
||
|
"Oba moduły mogą być realizowane zarówno z wykorzystaniem reguł jak i uczenia maszynowego.\n",
|
||
|
"Mogą one zostać również połączone w pojedynczy moduł zwany wówczas *menedżerem dialogu*.\n",
|
||
|
"\n",
|
||
|
"Przykład\n",
|
||
|
"--------\n",
|
||
|
"\n",
|
||
|
"Zaimplementujemy regułowe moduły monitora stanu dialogu oraz taktyki dialogowej a następnie\n",
|
||
|
"osadzimy je w środowisku *[ConvLab](https://github.com/ConvLab/ConvLab-3)*,\n",
|
||
|
"które służy do ewaluacji systemów dialogowych.\n",
|
||
|
"\n",
|
||
|
"**Uwaga:** Niektóre moduły środowiska *ConvLab* nie są zgodne z najnowszymi wersjami Pythona,\n",
|
||
|
"dlatego przed uruchomieniem poniższych przykładów należy się upewnić, że mają Państwo interpreter\n",
|
||
|
"Pythona w wersji 3.8.\n",
|
||
|
"Odpowiednią wersję Pythona można zainstalować korzystając m.in. z narzędzia [pyenv](https://github.com/pyenv/pyenv) oraz środowiska [conda](https://conda.io).\n",
|
||
|
"\n",
|
||
|
"Środowisko *ConvLab* można zainstalować korzystając z poniższych poleceń."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "4205706b",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"!mkdir -p l09\n",
|
||
|
"%cd l09\n",
|
||
|
"!git clone --depth 1 https://github.com/ConvLab/ConvLab-3\n",
|
||
|
"%cd ConvLab-3\n",
|
||
|
"!pip install -e .\n",
|
||
|
"%cd ../.."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "c14555bd",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Po zainstalowaniu środowiska `ConvLab` należy zrestartować interpreter Pythona (opcja *Kernel -> Restart* w Jupyter).\n",
|
||
|
"\n",
|
||
|
"Działanie zaimplementowanych modułów zilustrujemy, korzystając ze zbioru danych\n",
|
||
|
"[MultiWOZ](https://github.com/budzianowski/multiwoz) (Budzianowski i in., 2018), który zawiera\n",
|
||
|
"wypowiedzi dotyczące m.in. rezerwacji pokoi hotelowych, zamawiania biletów kolejowych oraz\n",
|
||
|
"rezerwacji stolików w restauracji.\n",
|
||
|
"\n",
|
||
|
"### Monitor Stanu Dialogu\n",
|
||
|
"\n",
|
||
|
"Do reprezentowania stanu dialogu użyjemy struktury danych wykorzystywanej w *ConvLab*."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "38c4de37",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from convlab.util.multiwoz.state import default_state\n",
|
||
|
"default_state()"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "09fecf16",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Metoda `update` naszego monitora stanu dialogu będzie przyjmować akty użytkownika i odpowiednio\n",
|
||
|
"modyfikować stan dialogu.\n",
|
||
|
"W przypadku aktów typu `inform` wartości slotów zostaną zapamiętane w słownikach odpowiadających\n",
|
||
|
"poszczególnym dziedzinom pod kluczem `belief_state`.\n",
|
||
|
"W przypadku aktów typu `request` sloty, o które pyta użytkownik zostaną zapisane pod kluczem\n",
|
||
|
"`request_state`.\n"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "172a883f",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"import json\n",
|
||
|
"import os\n",
|
||
|
"from convlab.dst.dst import DST\n",
|
||
|
"from convlab.dst.rule.multiwoz.dst_util import normalize_value\n",
|
||
|
"\n",
|
||
|
"class SimpleRuleDST(DST):\n",
|
||
|
" def __init__(self):\n",
|
||
|
" DST.__init__(self)\n",
|
||
|
" self.state = default_state()\n",
|
||
|
" self.value_dict = json.load(open('l09/ConvLab-3/data/multiwoz/value_dict.json'))\n",
|
||
|
"\n",
|
||
|
" def update(self, user_act=None):\n",
|
||
|
" for intent, domain, slot, value in user_act:\n",
|
||
|
" domain = domain.lower()\n",
|
||
|
" intent = intent.lower()\n",
|
||
|
" slot = slot.lower()\n",
|
||
|
" \n",
|
||
|
" if domain not in self.state['belief_state']:\n",
|
||
|
" continue\n",
|
||
|
"\n",
|
||
|
" if intent == 'inform':\n",
|
||
|
" if slot == 'none' or slot == '':\n",
|
||
|
" continue\n",
|
||
|
"\n",
|
||
|
" domain_dic = self.state['belief_state'][domain]\n",
|
||
|
"\n",
|
||
|
" if slot in domain_dic:\n",
|
||
|
" nvalue = normalize_value(self.value_dict, domain, slot, value)\n",
|
||
|
" self.state['belief_state'][domain][slot] = nvalue\n",
|
||
|
"\n",
|
||
|
" elif intent == 'request':\n",
|
||
|
" if domain not in self.state['request_state']:\n",
|
||
|
" self.state['request_state'][domain] = {}\n",
|
||
|
" if slot not in self.state['request_state'][domain]:\n",
|
||
|
" self.state['request_state'][domain][slot] = 0\n",
|
||
|
"\n",
|
||
|
" return self.state\n",
|
||
|
"\n",
|
||
|
" def init_session(self):\n",
|
||
|
" self.state = default_state()"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "a411a1ca",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"W definicji metody `update` zakładamy, że akty dialogowe przekazywane do monitora stanu dialogu z\n",
|
||
|
"modułu NLU są czteroelementowymi listami złożonymi z:\n",
|
||
|
"\n",
|
||
|
" - nazwy aktu użytkownika,\n",
|
||
|
" - nazwy dziedziny, której dotyczy wypowiedź,\n",
|
||
|
" - nazwy slotu,\n",
|
||
|
" - wartości slotu.\n",
|
||
|
"\n",
|
||
|
"Zobaczmy na kilku prostych przykładach jak stan dialogu zmienia się pod wpływem przekazanych aktów\n",
|
||
|
"użytkownika."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "2abb1707",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst = SimpleRuleDST()\n",
|
||
|
"dst.state"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "31312e0f",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst.update([['Inform', 'Hotel', 'Price Range', 'cheap'], ['Inform', 'Hotel', 'Parking', 'yes']])\n",
|
||
|
"dst.state['belief_state']['hotel']"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "38a02c80",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst.update([['Inform', 'Hotel', 'Area', 'north']])\n",
|
||
|
"dst.state['belief_state']['hotel']"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "23a47f33",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst.update([['Request', 'Hotel', 'Area', '?']])\n",
|
||
|
"dst.state['request_state']"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "9a0698f2",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst.update([['Inform', 'Hotel', 'Book Day', 'tuesday'], ['Inform', 'Hotel', 'Book People', '2'], ['Inform', 'Hotel', 'Book Stay', '4']])\n",
|
||
|
"dst.state['belief_state']['hotel']"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "1c5c8093",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"dst.state"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "b0814105",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Taktyka Prowadzenia Dialogu\n",
|
||
|
"\n",
|
||
|
"Prosta taktyka prowadzenia dialogu dla systemu rezerwacji pokoi hotelowych może składać się z następujących reguł:\n",
|
||
|
"\n",
|
||
|
" 1. Jeżeli użytkownik przekazał w ostatniej turze akt typu `Request`, to udziel odpowiedzi na jego\n",
|
||
|
" pytanie.\n",
|
||
|
"\n",
|
||
|
" 2. Jeżeli użytkownik przekazał w ostatniej turze akt typu `Inform`, to zaproponuj mu hotel\n",
|
||
|
" spełniający zdefiniowane przez niego kryteria.\n",
|
||
|
"\n",
|
||
|
" 3. Jeżeli użytkownik przekazał w ostatniej turze akt typu `Inform` zawierający szczegóły\n",
|
||
|
" rezerwacji, to zarezerwuj pokój.\n",
|
||
|
"\n",
|
||
|
"Metoda `predict` taktyki `SimpleRulePolicy` realizuje reguły przedstawione powyżej."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "14412255",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from collections import defaultdict\n",
|
||
|
"import copy\n",
|
||
|
"import json\n",
|
||
|
"from copy import deepcopy\n",
|
||
|
"\n",
|
||
|
"from convlab.policy.policy import Policy\n",
|
||
|
"from convlab.util.multiwoz.dbquery import Database\n",
|
||
|
"\n",
|
||
|
"\n",
|
||
|
"class SimpleRulePolicy(Policy):\n",
|
||
|
" def __init__(self):\n",
|
||
|
" Policy.__init__(self)\n",
|
||
|
" self.db = Database()\n",
|
||
|
"\n",
|
||
|
" def predict(self, state):\n",
|
||
|
" self.results = []\n",
|
||
|
" system_action = defaultdict(list)\n",
|
||
|
" user_action = defaultdict(list)\n",
|
||
|
"\n",
|
||
|
" for intent, domain, slot, value in state['user_action']:\n",
|
||
|
" user_action[(domain.lower(), intent.lower())].append((slot.lower(), value))\n",
|
||
|
"\n",
|
||
|
" for user_act in user_action:\n",
|
||
|
" self.update_system_action(user_act, user_action, state, system_action)\n",
|
||
|
"\n",
|
||
|
" # Reguła 3\n",
|
||
|
" if any(True for slots in user_action.values() for (slot, _) in slots if slot in ['book stay', 'book day', 'book people']):\n",
|
||
|
" if self.results:\n",
|
||
|
" system_action = {('Booking', 'Book'): [[\"Ref\", self.results[0].get('Ref', 'N/A')]]}\n",
|
||
|
"\n",
|
||
|
" system_acts = [[intent, domain, slot, value] for (domain, intent), slots in system_action.items() for slot, value in slots]\n",
|
||
|
" state['system_action'] = system_acts\n",
|
||
|
" return system_acts\n",
|
||
|
"\n",
|
||
|
" def update_system_action(self, user_act, user_action, state, system_action):\n",
|
||
|
" domain, intent = user_act\n",
|
||
|
" constraints = [(slot, value) for slot, value in state['belief_state'][domain.lower()].items() if value != '']\n",
|
||
|
" self.results = deepcopy(self.db.query(domain.lower(), constraints))\n",
|
||
|
"\n",
|
||
|
" # Reguła 1\n",
|
||
|
" if intent == 'request':\n",
|
||
|
" if len(self.results) == 0:\n",
|
||
|
" system_action[(domain, 'NoOffer')] = []\n",
|
||
|
" else:\n",
|
||
|
" for slot in user_action[user_act]: \n",
|
||
|
" if slot[0] in self.results[0]:\n",
|
||
|
" system_action[(domain, 'Inform')].append([slot[0], self.results[0].get(slot[0], 'unknown')])\n",
|
||
|
"\n",
|
||
|
" # Reguła 2\n",
|
||
|
" elif intent == 'inform':\n",
|
||
|
" if len(self.results) == 0:\n",
|
||
|
" system_action[(domain, 'NoOffer')] = []\n",
|
||
|
" else:\n",
|
||
|
" system_action[(domain, 'Inform')].append(['Choice', str(len(self.results))])\n",
|
||
|
" choice = self.results[0]\n",
|
||
|
"\n",
|
||
|
" if domain in [\"hotel\", \"attraction\", \"police\", \"restaurant\"]:\n",
|
||
|
" system_action[(domain, 'Recommend')].append(['Name', choice['name']])"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "bff4572c",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Podobnie jak w przypadku aktów użytkownika akty systemowe przekazywane do modułu NLG są czteroelementowymi listami złożonymi z:\n",
|
||
|
"\n",
|
||
|
" - nazwy aktu systemowe,\n",
|
||
|
" - nazwy dziedziny, której dotyczy wypowiedź,\n",
|
||
|
" - nazwy slotu,\n",
|
||
|
" - wartości slotu.\n",
|
||
|
"\n",
|
||
|
"Sprawdźmy jakie akty systemowe zwraca taktyka `SimpleRulePolicy` w odpowiedzi na zmieniający się stan dialogu."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "1b50240f",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from convlab.dialog_agent import PipelineAgent\n",
|
||
|
"dst.init_session()\n",
|
||
|
"policy = SimpleRulePolicy()\n",
|
||
|
"agent = PipelineAgent(nlu=None, dst=dst, policy=policy, nlg=None, name='sys')"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "116d54d7",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response([['Inform', 'Hotel', 'Price Range', 'cheap'], ['Inform', 'Hotel', 'Parking', 'yes']])"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "7d1c5be8",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response([['Request', 'Hotel', 'Area', '?']])"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "f296e283",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response([['Inform', 'Hotel', 'Area', 'centre']])"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "08d6bfaa",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response([['Inform', 'Hotel', 'Book Day', 'tuesday'], ['Inform', 'Hotel', 'Book People', '2'], ['Inform', 'Hotel', 'Book Stay', '4']])"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "ecf28c41",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Testy End-to-End\n",
|
||
|
"\n",
|
||
|
"Na koniec przeprowadźmy dialog łącząc w potok nasze moduły\n",
|
||
|
"z modułami NLU i NLG dostępnymi dla MultiWOZ w środowisku `ConvLab`."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "d5d95c0c",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from convlab.base_models.t5.nlu import T5NLU\n",
|
||
|
"from convlab.nlg.template.multiwoz import TemplateNLG\n",
|
||
|
"\n",
|
||
|
"nlu = T5NLU(speaker='user', context_window_size=0, model_name_or_path='ConvLab/t5-small-nlu-multiwoz21')\n",
|
||
|
"nlg = TemplateNLG(is_user=False)\n",
|
||
|
"agent = PipelineAgent(nlu=nlu, dst=dst, policy=policy, nlg=nlg, name='sys')"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "200e6941",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response(\"I need a cheap hotel with free parking .\")"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "da78fca0",
|
||
|
"metadata": {
|
||
|
"lines_to_next_cell": 0
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response(\"Yeah , could you book me a room for 2 people for 4 nights starting Tuesday ?\")"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": null,
|
||
|
"id": "fbb4e0cf",
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"agent.response(\"what is the hotel phone number ?\")"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"id": "6116cf5c",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Zauważmy, ze nasza prosta taktyka dialogowa zawiera wiele luk, do których należą m.in.:\n",
|
||
|
"\n",
|
||
|
" 1. Niezdolność do udzielenia odpowiedzi na przywitanie, prośbę o pomoc lub restart.\n",
|
||
|
"\n",
|
||
|
" 2. Brak reguł dopytujących użytkownika o szczegóły niezbędne do dokonania rezerwacji takie, jak długość pobytu czy liczba osób.\n",
|
||
|
"\n",
|
||
|
"Bardziej zaawansowane moduły zarządzania dialogiem zbudowane z wykorzystaniem reguł można znaleźć w\n",
|
||
|
"środowisku `ConvLab`. Należą do nich m.in. monitor [RuleDST](https://github.com/ConvLab/ConvLab-3/blob/master/convlab/dst/rule/multiwoz/dst.py) oraz taktyka [RuleBasedMultiwozBot](https://github.com/ConvLab/ConvLab-3/blob/master/convlab/policy/rule/multiwoz/rule_based_multiwoz_bot.py).\n",
|
||
|
"\n",
|
||
|
"Zadania\n",
|
||
|
"-------\n",
|
||
|
" 1. Zaimplementować w projekcie monitor stanu dialogu.\n",
|
||
|
"\n",
|
||
|
" 2. Zaimplementować w projekcie taktykę prowadzenia dialogu.\n",
|
||
|
"\n",
|
||
|
"Literatura\n",
|
||
|
"----------\n",
|
||
|
" 1. Pawel Budzianowski, Tsung-Hsien Wen, Bo-Hsiang Tseng, Iñigo Casanueva, Stefan Ultes, Osman Ramadan, Milica Gasic, MultiWOZ - A Large-Scale Multi-Domain Wizard-of-Oz Dataset for Task-Oriented Dialogue Modelling. EMNLP 2018, pp. 5016-5026\n",
|
||
|
" 2. Cathy Pearl, Basic principles for designing voice user interfaces, https://www.oreilly.com/content/basic-principles-for-designing-voice-user-interfaces/ data dostępu: 21 marca 2021\n",
|
||
|
" 3. Cathy Pearl, Designing Voice User Interfaces, Excerpts from Chapter 5: Advanced Voice User Interface Design, https://www.uxmatters.com/mt/archives/2018/01/designing-voice-user-interfaces.php data dostępu: 21 marca 2021"
|
||
|
]
|
||
|
}
|
||
|
],
|
||
|
"metadata": {
|
||
|
"jupytext": {
|
||
|
"cell_metadata_filter": "-all",
|
||
|
"main_language": "python",
|
||
|
"notebook_metadata_filter": "-all"
|
||
|
}
|
||
|
},
|
||
|
"nbformat": 4,
|
||
|
"nbformat_minor": 5
|
||
|
}
|