diff --git a/.gitignore b/.gitignore index 192c98f..885d588 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ export/* build/* rasa_storyteller.egg-info/* dist/* +.mypy_cache/* +*/export/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..21cd10c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [0.2.0] - 2020-07-01 +### Changed +- Logic of PySimpleGui UI is split from processing data +- Working with trees using [AnyTree](https://pypi.org/project/anytree/2.8.0/) is much easier now +- Import and export for nlu.json file is supported (#2) +- All code is formatted with [Black](https://black.readthedocs.io/en/stable/index.html#) + +## [0.1.0] - 2020-03-23 +### Added +- Initial release \ No newline at end of file diff --git a/core/__init__.py b/backend/__init__.py similarity index 100% rename from core/__init__.py rename to backend/__init__.py diff --git a/handlers/Handler.py b/backend/handlers/AbstractHandler.py similarity index 72% rename from handlers/Handler.py rename to backend/handlers/AbstractHandler.py index 2865107..4717643 100644 --- a/handlers/Handler.py +++ b/backend/handlers/AbstractHandler.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod -class Handler(ABC): - +class AbstractHandler(ABC): @abstractmethod def __init__(self, filename, *args): pass @@ -14,7 +13,3 @@ def import_data(self): @abstractmethod def export_data(self): pass - - @abstractmethod - def sort_alphabetically(self): - pass diff --git a/backend/handlers/Exporter.py b/backend/handlers/Exporter.py new file mode 100644 index 0000000..af4d042 --- /dev/null +++ b/backend/handlers/Exporter.py @@ -0,0 +1,41 @@ +import json +import shutil + +import yaml + + +class Exporter(object): + def __init__( + self, nlu, resp, stories, nlu_file_md, nlu_file_json, domain_file, stories_file + ): + self.nlu = nlu + self.resp = resp + self.stories = stories + self.nlu_file_md = nlu_file_md + self.nlu_file_json = nlu_file_json + self.domain_file = domain_file + self.stories_file = stories_file + + def export(self): + nlu_data = self.nlu.export_data() + resp_data = self.resp.export_data() + stories_data = self.stories.export_data() + + with open(self.nlu_file_md, "w", encoding="utf-8") as nf: + nlu_data["result"].seek(0) + shutil.copyfileobj(nlu_data["result"], nf) + + with open(self.nlu_file_json, "w", encoding="utf-8") as nfj: + nfj.write(json.dumps(nlu_data["result_json"], ensure_ascii=False, indent=4)) + + domain_data = { + "actions": resp_data["actions"], + "intents": nlu_data["intents"], + "responses": resp_data["responses"], + } + with open(self.domain_file, "w", encoding="utf-8") as df: + df.write(yaml.safe_dump(domain_data, allow_unicode=True)) + + with open(self.stories_file, "w", encoding="utf-8") as sf: + stories_data.seek(0) + shutil.copyfileobj(stories_data, sf) diff --git a/backend/handlers/ItemsWithExamplesHandler.py b/backend/handlers/ItemsWithExamplesHandler.py new file mode 100644 index 0000000..7552df7 --- /dev/null +++ b/backend/handlers/ItemsWithExamplesHandler.py @@ -0,0 +1,89 @@ +from abc import ABC + +from PySimpleGUI import TreeData +from anytree import Node +from anytree.search import find + +from backend.handlers.AbstractHandler import AbstractHandler +from backend.models.BaseNode import BaseNode, BaseItem +from backend.models.Intent import IntentNode +from common.constants import QUESTION_ICON, ANSWER_ICON + + +class ItemsWithExamplesHandler(AbstractHandler, ABC): + def __init__(self, filename, *args): + super().__init__(filename, *args) + self.filename = filename + self.items = [] + self.tree = Node("root", text="") + self.parent_nodes_class = BaseNode + self.parent_object_class = BaseItem + self.child_nodes_class = Node + + def add_to_items(self, key): + if key in self.items: + raise ValueError(f"There is already item with key {key} in this list.") + else: + self.items.append(key) + + def export_to_pysg_tree(self): + parent_icon = ( + QUESTION_ICON if self.parent_nodes_class == IntentNode else ANSWER_ICON + ) + tree_data = TreeData() + for item in self.tree.children: + tree_data.Insert( + parent="", + key=item.item.name, + text=item.item.name, + values=[], + icon=parent_icon, + ) + for example in item.children: + tree_data.Insert( + parent=item.item.name, + key=example.name, + text=example.name, + values=[], + icon=ANSWER_ICON, + ) + return tree_data + + def add_node_with_kids(self, parent_name, *kids): + item = self.parent_object_class(name=parent_name) + current_parent = self.parent_nodes_class(item, parent=self.tree) + self.add_to_items(parent_name) + for kid in kids: + self.child_nodes_class(name=kid, parent=current_parent) + self.add_to_items(kid) + + def add_example_to_node(self, parent, text): + selected_node = find(self.tree, lambda node: node.name == parent, maxlevel=3) + if isinstance(selected_node, self.parent_nodes_class): + parent = selected_node + elif isinstance(selected_node, self.child_nodes_class): + parent = selected_node.parent + else: + raise ValueError("Can't find item to add example.") + self.child_nodes_class(name=text, parent=parent) + self.add_to_items(text) + + def update_node_value(self, node, new_value): + node = find(self.tree, lambda n: n.name == node, maxlevel=3) + self.items.remove(node.name) + self.add_to_items(new_value) + node.name = new_value + if isinstance(node, self.parent_nodes_class): + node.item.name = new_value + for story_item in node.item.story_tree: + story_item.name = new_value + + def remove_node(self, node): + node = find(self.tree, lambda n: n.name == node, maxlevel=3) + if not hasattr(node, "item") or not node.item.story_tree: + node.parent = None + self.items.remove(node.name) + for kid in node.children: + self.items.remove(kid.name) + else: + raise ValueError("Item is used in stories and can't be removed.") diff --git a/backend/handlers/NLUHandler.py b/backend/handlers/NLUHandler.py new file mode 100644 index 0000000..c698016 --- /dev/null +++ b/backend/handlers/NLUHandler.py @@ -0,0 +1,77 @@ +import json +from io import StringIO # Python3 + +import markdown_generator as mg +from anytree.search import find + +from backend.handlers.ItemsWithExamplesHandler import ItemsWithExamplesHandler +from backend.models.Intent import IntentNode, Intent, IntentExample + + +class NLUHandler(ItemsWithExamplesHandler): + def __init__(self, filename, *args): + super().__init__(filename, *args) + self.parent_object_class = Intent + self.parent_nodes_class = IntentNode + self.child_nodes_class = IntentExample + + def import_data(self): + current_intent = None + try: + with open(self.filename, "r", encoding="utf-8") as df: + nlu = json.loads(df.read()) + for example in nlu["rasa_nlu_data"]["common_examples"]: + if example["intent"] not in self.items: + current_intent = IntentNode( + Intent(name=example["intent"]), parent=self.tree + ) + self.add_to_items(example["intent"]) + else: + current_intent = find( + self.tree, + lambda node: node.name == example["intent"], + maxlevel=2, + ) + IntentExample(name=example["text"], parent=current_intent) + self.add_to_items(example["text"]) + + except json.JSONDecodeError: # suggest it's markdown + with open(self.filename, "r", encoding="utf-8") as df: + nlu = df.readlines() + for line in nlu: + if line.startswith("## intent:"): + heading = line.split("## intent:")[1].strip() + current_intent = IntentNode( + Intent(name=heading), parent=self.tree + ) + self.add_to_items(heading) + if line.startswith("- "): + text_example = line.split("- ")[1].strip() + IntentExample(name=text_example, parent=current_intent) + self.add_to_items(text_example) + + def export_data(self): + result = StringIO() + intents = [] + writer = mg.Writer(result) + result_json = { + "rasa_nlu_data": { + "common_examples": [], + "regex_features": [], + "lookup_tables": [], + "entity_synonyms": [], + } + } + + for intent in self.tree.children: + intents.append(intent.item.name) + writer.write_heading(f"intent:{intent.item.name}", 2) + examples_list = [example.name.strip() for example in intent.children] + examples_md = [f"- {example}" for example in examples_list] + writer.writelines(examples_md) + for example in examples_list: + result_json["rasa_nlu_data"]["common_examples"].append( + {"intent": intent.item.name, "example": example} + ) + + return {"result": result, "result_json": result_json, "intents": intents} diff --git a/backend/handlers/ResponseHandler.py b/backend/handlers/ResponseHandler.py new file mode 100644 index 0000000..4d69c11 --- /dev/null +++ b/backend/handlers/ResponseHandler.py @@ -0,0 +1,37 @@ +import yaml + +from backend.handlers.ItemsWithExamplesHandler import ItemsWithExamplesHandler +from backend.models.Response import Response, ResponseNode, ResponseExample + + +class ResponseHandler(ItemsWithExamplesHandler): + def __init__(self, filename, *args): + super().__init__(filename, *args) + self.parent_object_class = Response + self.parent_nodes_class = ResponseNode + self.child_nodes_class = ResponseExample + + def import_data(self): + with open(self.filename, "r", encoding="utf-8") as domain_file: + domain_data = yaml.safe_load(domain_file.read()) + for response, texts in domain_data["responses"].items(): + response_name = response.split("utter_")[-1].strip() + current_response = ResponseNode( + Response(name=response_name), parent=self.tree + ) + self.add_to_items(response_name) + for text in texts: + ResponseExample(name=text["text"], parent=current_response) + self.add_to_items(text["text"]) + + def export_data(self): + responses = [] + result = {} + for response in self.tree.children: + responses.append(f"utter_{response.item.name}") + result[f"utter_{response.name}"] = [] + for kid in response.children: + result[f"utter_{response.item.name}"].append( + {"text": str(kid.name.strip())} + ) + return {"responses": result, "actions": responses} diff --git a/backend/handlers/StoriesHandler.py b/backend/handlers/StoriesHandler.py new file mode 100644 index 0000000..7199edd --- /dev/null +++ b/backend/handlers/StoriesHandler.py @@ -0,0 +1,180 @@ +from io import StringIO + +from PySimpleGUI import TreeData +from anytree.resolver import Resolver +from anytree.search import find + +from backend.handlers import ResponseHandler, NLUHandler +from backend.handlers.AbstractHandler import AbstractHandler +from backend.models.BaseNode import BaseNode, BaseItem +from backend.models.Intent import IntentStoryNode +from backend.models.Response import ResponseStoryNode +from common.constants import * + + +class StoriesHandler(AbstractHandler): + def __init__(self, filename, nlu: NLUHandler, resp: ResponseHandler): + super().__init__(filename) + self.filename = filename + self.tree = BaseNode(BaseItem(name="root")) + self.nlu = nlu + self.resp = resp + self.resolver = Resolver("name") + + def import_data(self): + with open(self.filename, "r", encoding="utf-8") as stories_file: + stories = stories_file.readlines() + for line in stories: + if line.startswith("## "): + # new story should start from root + current_response = self.tree + current_intent = None + + elif line.startswith("* "): # intent + intent_heading = line.split("* ")[-1].strip() + if find( + current_response, + lambda n: n.name == intent_heading + and isinstance(n, IntentStoryNode), + maxlevel=2, + ): + current_intent = find( + current_response, + lambda n: n.name == intent_heading + and isinstance(n, IntentStoryNode), + maxlevel=2, + ) + else: + nlu_node = find( + self.nlu.tree, lambda n: n.name == intent_heading + ) + current_intent = IntentStoryNode( + item=nlu_node.item, parent=current_response + ) + + elif line.strip().startswith("- "): # response + response_heading = line.split("- ")[-1].split("utter_")[-1].strip() + if find( + current_intent, + lambda n: n.name == response_heading + and isinstance(n, ResponseStoryNode), + maxlevel=2, + ): + current_response = find( + current_intent, + lambda n: n.name == response_heading + and isinstance(n, ResponseStoryNode), + maxlevel=2, + ) + else: + resp_node = find( + self.resp.tree, lambda n: n.name == response_heading + ) + current_response = ResponseStoryNode( + item=resp_node.item, parent=current_intent + ) + + def export_to_pysg_tree(self): + def insert_branch(branch, parent): + values = " | ".join([kid.name for kid in branch.item.own_tree.children]) + branch_type = ( + TYPE_INTENT if isinstance(branch, IntentStoryNode) else TYPE_RESPONSE + ) + icon = QUESTION_ICON if branch_type == TYPE_INTENT else ANSWER_ICON + tree_data.Insert( + parent=parent, + key=branch.id, + text=branch.item.name, + values=[branch_type, values], + icon=icon, + ) + for child in branch.children: + insert_branch(child, branch.id) + + tree_data = TreeData() + for item in self.tree.children: + insert_branch(item, "") + + self.export_data() + return tree_data + + def export_data(self): + result = StringIO() + for leaf in self.tree.leaves: + if not leaf.siblings or leaf == leaf.parent.children[-1]: + path = list(leaf.iter_path_reverse())[:-1][::-1] + story_heading = "-".join( + [ + item.item.name + for item in path + if isinstance(item, IntentStoryNode) + ] + ) + result.write(f"\n\n## {story_heading}") + for item in path: + if isinstance(item, IntentStoryNode): + result.write(f"\n* {item.item.name}") + elif isinstance(item, ResponseStoryNode): + if item.siblings: + for sibling in item.parent.children: + result.write(f"\n - utter_{sibling.item.name}") + else: + result.write(f"\n - utter_{item.item.name}") + return result + + def get_parent_node_by_object_id(self, object_id): + parent_node = find(self.tree, lambda n: n.id == object_id).parent + return parent_node + + def get_object_type_and_handler_by_id(self, object_id): + node = find(self.tree, lambda n: n.id == object_id) + if isinstance(node, ResponseStoryNode): + result = {"type": TYPE_RESPONSE, "handler": self.nlu} + elif isinstance(node, IntentStoryNode): + result = {"type": TYPE_INTENT, "handler": self.resp} + else: + result = {"type": TYPE_ROOT, "handler": None} + return result + + def get_child_type_and_handler_by_id(self, object_id): + node = find(self.tree, lambda n: n.id == object_id) + child_type = None + child_handler = None + if isinstance(node, ResponseStoryNode) or isinstance(node, BaseNode): + child_type = TYPE_INTENT + child_handler = self.nlu + elif isinstance(node, IntentStoryNode): + child_type = TYPE_RESPONSE + child_handler = self.resp + return child_type, child_handler + + def get_available_children_by_parent_id(self, parent_object_id): + parent_node = find(self.tree, lambda n: n.id == parent_object_id) + available_values = None + if isinstance(parent_node, ResponseStoryNode) or isinstance( + parent_node, BaseNode + ): + available_values = sorted([child.name for child in self.nlu.tree.children]) + elif isinstance(parent_node, IntentStoryNode): + available_values = sorted([child.name for child in self.resp.tree.children]) + return available_values + + def add_item(self, parent_object_id, text): + parent_node = find(self.tree, lambda n: n.id == parent_object_id) + existing_node = find(parent_node, lambda n: n.name == text, maxlevel=2) + new_item = None + if existing_node: + new_item = existing_node + elif isinstance(parent_node, ResponseStoryNode) or isinstance( + parent_node, BaseNode + ): + add_node = find(self.nlu.tree, lambda n: n.name == text) + new_item = IntentStoryNode(item=add_node.item, parent=parent_node) + elif isinstance(parent_node, IntentStoryNode): + add_node = find(self.resp.tree, lambda n: n.name == text) + new_item = ResponseStoryNode(item=add_node.item, parent=parent_node) + return new_item + + def remove_item(self, node_id): + node = find(self.tree, lambda n: n.id == node_id) + node.parent = None diff --git a/backend/handlers/__init__.py b/backend/handlers/__init__.py new file mode 100644 index 0000000..a901347 --- /dev/null +++ b/backend/handlers/__init__.py @@ -0,0 +1 @@ +from backend.handlers import * diff --git a/backend/models/BaseNode.py b/backend/models/BaseNode.py new file mode 100644 index 0000000..006247f --- /dev/null +++ b/backend/models/BaseNode.py @@ -0,0 +1,30 @@ +from uuid import uuid4 + +from anytree import NodeMixin + + +class BaseItem: + def __init__(self, name): + self.name = name + self.node_tree = None + self.story_tree = None + + def __repr__(self): + return str(self.name) + + +class BaseNode(NodeMixin): + def __init__(self, item, parent=None): + self.item = item + self.parent = parent + self.name = self.item.name + self.id = str(uuid4()) + + def _pre_detach(self, parent): + self.item.node_tree = None + + def _pre_attach(self, parent): + self.item.intent_tree = self + + def __repr__(self): + return str(self.name) diff --git a/backend/models/Intent.py b/backend/models/Intent.py new file mode 100644 index 0000000..341ca66 --- /dev/null +++ b/backend/models/Intent.py @@ -0,0 +1,56 @@ +from uuid import uuid4 + +from anytree import NodeMixin + + +class Intent: + def __init__(self, name): + self.name = name + self.own_tree = None + self.story_tree = [] + + def __repr__(self): + return f"intent_object_{self.name}" + + +class IntentNode(NodeMixin): + def __init__(self, item: Intent, parent=None): + self.item = item + self.parent = parent + self.name = self.item.name + + def _pre_detach(self, parent): + self.item.own_tree = None + + def _pre_attach(self, parent): + self.item.own_tree = self + + def __repr__(self): + return f"intent_node_{self.name}" + + +class IntentStoryNode(NodeMixin): + def __init__(self, item: Intent, parent=None): + self.item = item + self.parent = parent + self.name = self.item.name + self.id = str(uuid4()) + + def _pre_detach(self, parent): + self.item.story_tree.remove(self) + + def _pre_attach(self, parent): + self.item.story_tree.append(self) + + def __repr__(self): + return f"story_response_node_{self.name}_{self.id}" + + +class IntentExample(NodeMixin): + def __init__(self, name, parent: IntentNode): + super(IntentExample, self).__init__() + self.name = name + self.parent = parent + + def __repr__(self): + return f"intent_example_{self.name}" diff --git a/backend/models/Response.py b/backend/models/Response.py new file mode 100644 index 0000000..07efef1 --- /dev/null +++ b/backend/models/Response.py @@ -0,0 +1,56 @@ +from uuid import uuid4 + +from anytree import NodeMixin + + +class Response: + def __init__(self, name): + self.name = name + self.own_tree = None + self.story_tree = [] + + def __repr__(self): + return f"response_object_{self.name}" + + +class ResponseNode(NodeMixin): + def __init__(self, item, parent=None): + self.item = item + self.parent = parent + self.name = self.item.name + + def _pre_detach(self, parent): + self.item.own_tree = None + + def _pre_attach(self, parent): + self.item.own_tree = self + + def __repr__(self): + return f"response_node_{self.name}" + + +class ResponseExample(NodeMixin): + def __init__(self, name, parent: ResponseNode): + super(ResponseExample, self).__init__() + self.name = name + self.parent = parent + + def __repr__(self): + return f"response_example_{self.name}" + + +class ResponseStoryNode(NodeMixin): + def __init__(self, item: Response, parent=None): + self.item = item + self.parent = parent + self.name = self.item.name + self.id = str(uuid4()) + + def _pre_detach(self, parent): + self.item.story_tree.remove(self) + + def _pre_attach(self, parent): + self.item.story_tree.append(self) + + def __repr__(self): + return f"story_response_node_{self.name}_{self.id}" diff --git a/forms/__init__.py b/backend/models/__init__.py similarity index 100% rename from forms/__init__.py rename to backend/models/__init__.py diff --git a/common/constants.py b/common/constants.py index bba70e5..f3a7426 100644 --- a/common/constants.py +++ b/common/constants.py @@ -1,14 +1,16 @@ # Text messages, may be localized if need -APP_NAME = 'RASA storyteller' +APP_NAME = "RASA storyteller" -LOCATE_FILES_WINDOW_NAME = 'Locate your files' +LOCATE_FILES_WINDOW_NAME = "Locate your files" -TAB_INTENTS_HEADING = 'Intents' -TAB_INTENTS_DESCRIPTION = 'Natural Language Understanding: specify intents which will be understood by bot' -TAB_RESPONSES_HEADING = 'Answers' -TAB_RESPONSES_DESCRIPTION = 'Prepare your bot`s responses here' -TAB_STORIES_HEADING = 'Stories' -TAB_STORIES_DESCRIPTION = 'Construct your own complex stories' +TAB_INTENTS_HEADING = "Intents" +TAB_INTENTS_DESCRIPTION = ( + "Natural Language Understanding: specify intents which will be understood by bot" +) +TAB_RESPONSES_HEADING = "Answers" +TAB_RESPONSES_DESCRIPTION = "Prepare your bot`s responses here" +TAB_STORIES_HEADING = "Stories" +TAB_STORIES_DESCRIPTION = "Construct your own complex stories" LOCATE_FILE_TEXT = "Locate your {filename} file" @@ -16,41 +18,39 @@ MSG_FIRST_SELECT_ITEM = "First you need to select an item" MSG_EXPORT_SUCCESSFUL = "Export completed successfully" -FORM_NAME_ADD_INTENT = 'Add intent' -FORM_NAME_ADD_RESPONSE = 'Add answer' -FORM_NAME_EDIT_INTENT = 'Edit intent' -FORM_NAME_EDIT_ANSWER = 'Edit answer' -FORM_NAME_ADD_CHILD = 'Add child' -FORM_NAME_ADD_SIBLING = 'Add sibling' -FORM_NAME_ADD_STORY_ITEM = 'Add story item' +FORM_NAME_ADD_INTENT = "Add intent" +FORM_NAME_ADD_RESPONSE = "Add answer" +FORM_NAME_EDIT_INTENT = "Edit intent" +FORM_NAME_EDIT_ANSWER = "Edit answer" +FORM_NAME_ADD_CHILD = "Add child" +FORM_NAME_ADD_SIBLING = "Add sibling" +FORM_NAME_ADD_STORY_ITEM = "Add story item" # App theme, supported by PySimpleGui -APP_THEME = 'DarkBlue2' -APP_THEME_BG_COLOR = '#242834' +APP_THEME = "DarkBlue2" +APP_THEME_BG_COLOR = "#242834" # Actions and buttons names ACTION_SUBMIT = "Submit" ACTION_CANCEL = "Cancel" -ACTION_ADD_INTENT = 'Add intent' -ACTION_ADD_INTENT_EXAMPLE = 'Add intent example' -ACTION_UPDATE_INTENT = 'Update intent' -ACTION_REMOVE_INTENT = 'Remove intent' -ACTION_ADD_RESPONSE = 'Add answer' -ACTION_ADD_RESPONSE_EXAMPLE = 'Add answer example' -ACTION_UPDATE_RESPONSE = 'Update answer' -ACTION_REMOVE_RESPONSE = 'Remove answer' -ACTION_ADD_CHILD = 'Add child' -ACTION_ADD_SIBLING = 'Add sibling' -ACTION_REMOVE_STORY_ITEM = 'Remove item' -ACTION_MOVE_STORY_ITEM_UP = 'Move up' -ACTION_MOVE_STORY_ITEM_DOWN = 'Move down' -ACTION_EXPORT_DATA = 'Export data' -ACTION_CLOSE_WINDOW = 'Close' -ACTION_ADD_NEW_ITEM_AS_A_CHILD = 'Add new intent/answer as a kid' -BUTTON_ADD_EXAMPLE = 'Add example' +ACTION_ADD_INTENT = "Add intent" +ACTION_ADD_INTENT_EXAMPLE = "Add intent example" +ACTION_UPDATE_INTENT = "Update intent" +ACTION_REMOVE_INTENT = "Remove intent" +ACTION_ADD_RESPONSE = "Add answer" +ACTION_ADD_RESPONSE_EXAMPLE = "Add answer example" +ACTION_UPDATE_RESPONSE = "Update answer" +ACTION_REMOVE_RESPONSE = "Remove answer" +ACTION_ADD_CHILD = "Add child" +ACTION_ADD_SIBLING = "Add sibling" +ACTION_REMOVE_STORY_ITEM = "Remove item" +ACTION_EXPORT_DATA = "Export data" +ACTION_CLOSE_WINDOW = "Close" +ACTION_ADD_NEW_ITEM_AS_A_CHILD = "Add new intent/answer as a kid" +BUTTON_ADD_EXAMPLE = "Add example" -DEFAULT_FORM_SUBMIT_ACTIONS = (ACTION_SUBMIT, '\r', '\n') -DEFAULT_FORM_CANCEL_ACTIONS = (ACTION_CANCEL, 'Escape:27') +DEFAULT_FORM_SUBMIT_ACTIONS = (ACTION_SUBMIT, "\r", "\n") +DEFAULT_FORM_CANCEL_ACTIONS = (ACTION_CANCEL, "Escape:27") # Object types TYPE_INTENT = "intent" @@ -58,13 +58,13 @@ TYPE_ROOT = "root" # Base64 versions of images (PNG files) for buttons and icons -green_button = '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' -blue_button = 'iVBORw0KGgoAAAANSUhEUgAAAKAAAAAkCAYAAAAO7jHjAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH5AMUBTQ33IbdPwAABo1JREFUeNrtW72PHDUU//1sz272TiEgRBUpBEWRECJN/gZ6RAEdoaRHouAPoKGgoo9Ew0eoaJBQJCREh4SoQJAoig7l+5PL5T52xn4UM7OZnR3PePZ27zacf8Xdzdh+7+c3757tZ5sffPQp1taOIx3vIiLioKBNgq3NxzCwloCMSH4sIucAOAA8bIIR/0sIAAXyTwCfDYfDLYoIPvzk8y+ds+8fNruIowNSfQedvMe33rnwwumzb/4N4GWUHhoRsTyUI+yTuzc3zppH926vnTrzhmitjYgIQILIXdGHsrw+UFffSaVu/Vka5AF+nRRA2F3PJ7+rjACk1iEWikq9UmvP4o9q+YxtivYhvJrs16S3Wthm20bOLd+lzZ7wtWtyglq9GR2iSNJaK/dubRw3uzvbBoAm6RfQpaDpHXuUdepkWPuQcq9uT4dYa8eaIDbIZUP7vry9ehsEMVBGE4fQ2X5ju8DGde65rfXe7o4xWbpXOB8bKgdCAtv1qVfn4otci0Iot2XLCJHZpWcZPEI5BnAjCJLI0lSbdDxOALAceecCA3WzuXymXVd0XQYWIX8ZHCsyJ3bazwiwDPSN8sVok6VjY2yWGjD3yK7pQB8efcoPN+dz0OFifjwfLLsgIAmQzLI0Mc45w2JcZrEGCRHijblTRaW8ysuZ791WFkgFPpltY0P5zFChfnpS1A+2XYmmVUGnNo9MzlHWxa3NHG027tBJgiCctdo4Z3U5AMvEgF0DZT3mVhiyQmIyt6zUKUPwZBWlpkV5yXs+1FSbulNVuamajLY+NhuPPiPXFyZe7tU6pQxVq1d9H+o8vmU0KvK7nKNFJ1GTWZXbYGOq2ip5WiaLxZlzzhhxolFMCtuM3xw9fAZgx/u2j+bLHzTVqZf5UphskN3Vx/rHqcupO7x4fjfJrTtH+a7Ovy0l29SPrslYU+Cov2vLVYWmF8rJfnP/WURAEaeMQFiMyQFK2jrXZYA+H70eHZoiU+jMd968Q0i/gvMmgX3uw5EzTzJlK1/9WT15277698G9mAOKiDIASJYRcFmbIPMsP/ZjkKMJVn7O1/aAeE78DTDFG+wrDRMREYzC1yoOOImA+03DrBp8s5mIQ4SgSMMoAKAp8tLg1KS0bz5kdXNpq8mqitW13VLAcickd0QDOFAp6MGolrNr2gXvE1OackRt9ZraVNF1AqFar4mnb1+vK//Wlrfru7Pv63eTzXw86Hnuo9+X0Wg71VDX3cbRV/cZrLUQERgRgR6MoAdDOGcRZ4IRy4WASkMPLQBAaTMAtQFEoutFHAxEoJSGMkmxCgYqecBVwFFaPrQlfiWw7qJ0HgAkXwWX2k1Oh1M5pNVww9Vgcbh9XeaxoMOxb3kiShUBL3dARVApwDkwaEM9ImJOiIBKFclowACEIqGUmuzfLccHDzrsd+lb1WG+jdeiOfuyHYvmnUPKAwpUecADYEhAaw2jFUpn7OeE86RmQuphAUaZh+PzhL7/ZMv4VmEonc8VIl0RBQ0AMUZjkCQgBYoKqjgyNXWgAfUH3y2jrrzRvJ3fT/tFG75PDjAkooXwXWZE9+X9FgApT2blzufEQYQQm4EAjDGGw4HhsWECowitFZTi5MjM5GSNX37wdLbtesPzvh/Q51rEvHIWaaO2w2NtnEK5l8+5AwpEBM4JrHWwTgCXwSQJzJPHD+366JhdXx+B4pAkGloVTri0+WDEUYEUDuicwDqHNLMQIRTFbj66b82Duzef/vrzD1fefvfCK8fXRzJINBOjoZTK54PF3l1ERF9IMf46yZ0vyyzGqZWnO3v85fL3V29cv7JpBsPh9qWLX3zzYGvr/KnXzhwTm4lSpKocmcnngtEJI/pBJhPA3AmdE1EmURvXru5c/vbiV8NjoycmTVOdJIOfbty9c/vWw4enxVlBdV+kvhLxLkoiIqTxzxIkhVoz29u9YZLBj1mWaUNQsiyV0WjNDoZDQFy1wdTVjTgUR/SDTC4M5r/yfPNY68xmqVXGwFBr51KbKkUHUsSKoNyqm7kJLTHoRYSh6nhSupOIUIvSGiKSUSlrTDKwLkt37l2/+tfJ18+d1WtrGqhcYeHsSniVdowjVg0yO/pOpoLUzlnc3rh2leSu1okze9tbmdLa/vPH719v3r9zcu3ESyfEOanO/2YT0hERPVA4IJXi9r+PNh/fuXlJaT3e294CAbwIYKCTgbbp+DyAV9F1Qzsioh8Ez3xqQyeD32w6zgCk/wGsinrQyob3oQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wMy0yMFQwNTo1Mjo1NS0wNzowMEItRZcAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDMtMjBUMDU6NTI6NTUtMDc6MDAzcP0rAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAABJRU5ErkJggg==' -black_button = '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' -orange_button = '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' +green_button = "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" +blue_button = "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" +black_button = "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" +orange_button = "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" -ANSWER_ICON = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADc0lEQVR42l2Ta2hbZRjH/+85OSeJp0mT1GTN0qUim810WDadbpWBgyatMEYn88ZmSzuqs25U2AcR/TAGwph4qZNOMuxUnH4pa52D2XWgwwmbXdt1sRVbakRN2+S0uTWn536OZ6lM5wMPvDy8/Pg/lz/B/6NudzNca1sqQ449lR4vd7tUyOeEQkoawPLcIH49/+1/v5M7r/pWFrpyYWf3M9GGhs24LxiCWmSg0QSmKWD2ryR+uPELxuPnhkGzuzDxufIv4NGDDAw9daz/Df+T1TXou8bj50wBz2/wYZwX8Lug40RsAwRaRv/ELOKvfsCDokO48bG6Cnh4/6V3B49HfZQTX40tIi2b+LKpFg96HZjKSTjyYwrzRQXPbfShcZMf/YlZnGg/NoxbX8QI6ttiHW+2D4U31eG7yQJUisaSrONsYxiP+J0Y5UV0X52DYZjwMhRYBji4Yx16zl7ExVPnmgjZdiR+qPe1zvmcBs4A3HYayYKKGStzKytYWpFQX7MGT4U58CuKlSocLIuI38TRl3tOk9rW4+nmrn0BIhOM8BISWRm1bjve21qFB7xOTOdEq4U/MW3Vg3YTT9cFoVCAUVWFy+98mCGhrh4xunev41pGxEI2D7EkWHI1XG/bis3VLowvLGN//01MLRRxb9ALBTSejaxFIBTEZz29Etn4+qdics39DkZT4KaBrGhANHSMdT5RBoymS3hhIIGZhTxqA5XICiIU3UB3SyO+6TsjkZpXTqWlhyIBSjHLvWWWJcjWwEY7Hl8FZG4DJjEzl0c4UAFFlSFICqojEYiXhzLEse/9uC+2o9MoirCZJhZLClRLwciB7WXAWEYoA6bnlhD2uyFrGmzWKhRrG8tXfjpNuN1vx1yN24dojgMRJPAlFYZp4HrHtjJgwjqkF7+eROIPCxBwWQAdNo8PajYF4cpYU/mQuPaPLvl2NkQpUcV8OguHncH3rY+VAbcWBbSdn8TNJI91lgKDqwAh1oqvjgwLZw7FVgEdJxnK4055t9T7FWuIS4s83opugcdhR16ScXRoHL5KN5z3OK3jV1BITPFGvhgS+g6rd8zk7IqzNsZ2oWL9+ihlKSiZLEqMB6yhwqXlwRIDmgUTfksOa6q2S+x9Sbnbjf8Ed/iTZpvP00Lb6D2EYct2NlVF0DV9QMvmB4WTB+6y89/rZI8dnqV10wAAAABJRU5ErkJggg==' -QUESTION_ICON = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB3ElEQVR42mNkwAHit32aICDKno8s9vzZh9ZV/uI1yGKM2DRHbP7UmmDEXcTOzMjxjQmo5P9/BtZ/DAxH7n18te3ip8YzWfLTcBoQu/VTSa8Hb/fJdwwMP/4yMPz+z8DwHYhZgCql+RkY1p95/+Xu4x8Ld0RL5WA1IHj9u3vF3oKKtz8wMJz9AFGgx8vA8OHbfwYONkaGr19/M9x8/PX9XFdBIawGuK58/d1Qno/j2vOP57YEihmDxNJWPPyvbyvH8OMXA8Onz38YXn369mW6DT8vzjBAB6mrH/3XMJVlYAJ65T3QBTfufPi3KkCMmSgDDHtu/l+SpcYw6+ovBj4pNoYrx168fP7wff2JEq2ZBA0AaZ6cqcbQdPEfw0eg0ifXPjE8nbRHj+FSyGW80QgG5tvNDEOVTopaqYC5jx98Yrjet8uK4Uz4cYLpAAT4+p7niqsKTYLxX95+t+9TkaQzujp8BiQAKVBKfAvEPEC8AmjABKINIBbgNcC87vQjIQNF2ZvHb7++t+2CNMPVzN9EG2Bdd/6mRbaB2urTDAyPrr9iYDx4+sX/LT6SRBtgOvP+/6fs4gzPnn5hYHj2mYHh84tPDIus+UnyguTCz4+eP3oly/Dp11OGbk0ZbGoA9qfAETkQhkkAAAAASUVORK5CYII=' +ANSWER_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADc0lEQVR42l2Ta2hbZRjH/+85OSeJp0mT1GTN0qUim810WDadbpWBgyatMEYn88ZmSzuqs25U2AcR/TAGwph4qZNOMuxUnH4pa52D2XWgwwmbXdt1sRVbakRN2+S0uTWn536OZ6lM5wMPvDy8/Pg/lz/B/6NudzNca1sqQ449lR4vd7tUyOeEQkoawPLcIH49/+1/v5M7r/pWFrpyYWf3M9GGhs24LxiCWmSg0QSmKWD2ryR+uPELxuPnhkGzuzDxufIv4NGDDAw9daz/Df+T1TXou8bj50wBz2/wYZwX8Lug40RsAwRaRv/ELOKvfsCDokO48bG6Cnh4/6V3B49HfZQTX40tIi2b+LKpFg96HZjKSTjyYwrzRQXPbfShcZMf/YlZnGg/NoxbX8QI6ttiHW+2D4U31eG7yQJUisaSrONsYxiP+J0Y5UV0X52DYZjwMhRYBji4Yx16zl7ExVPnmgjZdiR+qPe1zvmcBs4A3HYayYKKGStzKytYWpFQX7MGT4U58CuKlSocLIuI38TRl3tOk9rW4+nmrn0BIhOM8BISWRm1bjve21qFB7xOTOdEq4U/MW3Vg3YTT9cFoVCAUVWFy+98mCGhrh4xunev41pGxEI2D7EkWHI1XG/bis3VLowvLGN//01MLRRxb9ALBTSejaxFIBTEZz29Etn4+qdics39DkZT4KaBrGhANHSMdT5RBoymS3hhIIGZhTxqA5XICiIU3UB3SyO+6TsjkZpXTqWlhyIBSjHLvWWWJcjWwEY7Hl8FZG4DJjEzl0c4UAFFlSFICqojEYiXhzLEse/9uC+2o9MoirCZJhZLClRLwciB7WXAWEYoA6bnlhD2uyFrGmzWKhRrG8tXfjpNuN1vx1yN24dojgMRJPAlFYZp4HrHtjJgwjqkF7+eROIPCxBwWQAdNo8PajYF4cpYU/mQuPaPLvl2NkQpUcV8OguHncH3rY+VAbcWBbSdn8TNJI91lgKDqwAh1oqvjgwLZw7FVgEdJxnK4055t9T7FWuIS4s83opugcdhR16ScXRoHL5KN5z3OK3jV1BITPFGvhgS+g6rd8zk7IqzNsZ2oWL9+ihlKSiZLEqMB6yhwqXlwRIDmgUTfksOa6q2S+x9Sbnbjf8Ed/iTZpvP00Lb6D2EYct2NlVF0DV9QMvmB4WTB+6y89/rZI8dnqV10wAAAABJRU5ErkJggg==" +QUESTION_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB3ElEQVR42mNkwAHit32aICDKno8s9vzZh9ZV/uI1yGKM2DRHbP7UmmDEXcTOzMjxjQmo5P9/BtZ/DAxH7n18te3ip8YzWfLTcBoQu/VTSa8Hb/fJdwwMP/4yMPz+z8DwHYhZgCql+RkY1p95/+Xu4x8Ld0RL5WA1IHj9u3vF3oKKtz8wMJz9AFGgx8vA8OHbfwYONkaGr19/M9x8/PX9XFdBIawGuK58/d1Qno/j2vOP57YEihmDxNJWPPyvbyvH8OMXA8Onz38YXn369mW6DT8vzjBAB6mrH/3XMJVlYAJ65T3QBTfufPi3KkCMmSgDDHtu/l+SpcYw6+ovBj4pNoYrx168fP7wff2JEq2ZBA0AaZ6cqcbQdPEfw0eg0ifXPjE8nbRHj+FSyGW80QgG5tvNDEOVTopaqYC5jx98Yrjet8uK4Uz4cYLpAAT4+p7niqsKTYLxX95+t+9TkaQzujp8BiQAKVBKfAvEPEC8AmjABKINIBbgNcC87vQjIQNF2ZvHb7++t+2CNMPVzN9EG2Bdd/6mRbaB2urTDAyPrr9iYDx4+sX/LT6SRBtgOvP+/6fs4gzPnn5hYHj2mYHh84tPDIus+UnyguTCz4+eP3oly/Dp11OGbk0ZbGoA9qfAETkQhkkAAAAASUVORK5CYII=" # Main trees parameters TREE_LAYOUT_COMMON_PARAMS = { @@ -73,13 +73,13 @@ "num_rows": 40, "show_expanded": False, "enable_events": True, - "justification": 'left' + "justification": "left", } -INTENT_TREE_KEY = 'active_intent' -RESPONSE_TREE_KEY = 'active_answer' -STORIES_TREE_KEY = 'active_story_item' +INTENT_TREE_KEY = "active_intent" +RESPONSE_TREE_KEY = "active_answer" +STORIES_TREE_KEY = "active_story_item" -NLU_FILE_KEY = 'nlu_file' -DOMAIN_FILE_KEY = 'domain_file' -STORIES_FILE_KEY = 'stories_file' +NLU_FILE_KEY = "nlu_file" +DOMAIN_FILE_KEY = "domain_file" +STORIES_FILE_KEY = "stories_file" diff --git a/core/EditableTreeData.py b/core/EditableTreeData.py deleted file mode 100644 index 0118d99..0000000 --- a/core/EditableTreeData.py +++ /dev/null @@ -1,51 +0,0 @@ -import json - -import PySimpleGUI as sg - - -class EditableTreeData(sg.TreeData): - def __init__(self): - super().__init__() - - def _show_node(self, node, level): - node_data = { - "key": node.key, - "text": node.text, - "values": [value for value in node.values], - "children": [self._show_node(child, level + 1) for child in node.children] - } - return node_data - - def get_node_data_by_key(self, key): - node = self.tree_dict[key] - return {"key": key, - "text": node.text, - "values": node.values, - "icon": node.icon, - "children": node.children} - - def Insert(self, parent, key, text, values, icon=None): - node = self.Node(parent, key, text, values, icon) - self.tree_dict[key] = node - parent_node = self.tree_dict[parent] - parent_node._Add(node) - return node.key - - def update_node(self, key, text, values, icon=None): - node = self.tree_dict[key] - node.text = text - node.values = values - node.icon = icon - return node - - def remove_node(self, key): - node = self.tree_dict[key] - self.tree_dict[node.parent].children.remove(node) - self.tree_dict.pop(key) - del node - - def dump(self): - return self._show_node(self.root_node, 1)['children'] - - def __repr__(self): - return json.dumps(self._show_node(self.root_node, 1), indent=4, ensure_ascii=False) diff --git a/core/ExtendedTree.py b/core/ExtendedTree.py deleted file mode 100644 index beb0fd1..0000000 --- a/core/ExtendedTree.py +++ /dev/null @@ -1,135 +0,0 @@ -from uuid import uuid4 - -import PySimpleGUI as sg - -from common.constants import * - - -class ExtendedTree(sg.Tree): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def see(self, item): - id = self.KeyToID[item] - self.TKTreeview.see(id) - - def get_selection(self): - return [self.IdToKey[item] for item in self.TKTreeview.selection()] - - def get_parent(self, item): - return self.IdToKey[self.TKTreeview.parent(self.KeyToID[item])] - - def get_family_tree(self, item, tree=''): - if item == '': - return '' - if tree == '': - tree += self.TreeData.get_node_data_by_key(item)['text'] - parent = self.get_parent(item) - parent_heading = self.TreeData.get_node_data_by_key(self.get_parent(item))['text'] - if parent_heading != 'root': - tree = parent_heading + '-' + tree - return self.get_family_tree(parent, tree).strip('-') - else: - return tree.strip('-') - - def get_family_tree_in_story_format(self, item, tree=None): - if tree is None: - tree = [] - - node = self.TreeData.get_node_data_by_key(item) - if not tree: - tree.append((node['text'], node['values'][0])) - - parent = self.TreeData.get_node_data_by_key(self.get_parent(item)) - if parent['text'] != 'root': - if node['values'][0] == TYPE_RESPONSE: - for sibling_key in self.get_older_siblings(item): - sibling = self.TreeData.get_node_data_by_key(sibling_key) - tree.append((sibling['text'], sibling['values'][0])) - tree.append((parent['text'], parent['values'][0])) - return self.get_family_tree_in_story_format(self.get_parent(item), tree) - else: - return tree - - def get_previous_sibling(self, item, parent_if_none=False): - prev = self.TKTreeview.prev(self.KeyToID[item]) - if prev == '': - result = self.get_parent(item) if parent_if_none else None - else: - result = self.IdToKey[prev] - return result - - def get_older_siblings(self, item, older_siblings=None): - if older_siblings is None: - older_siblings = [] - if self.get_previous_sibling(item): - older_siblings.append(self.get_previous_sibling(item)) - self.get_older_siblings(self.get_previous_sibling(item), older_siblings) - return older_siblings - - def get_next_sibling(self, item): - next_node = self.TKTreeview.next(self.KeyToID[item]) - return self.IdToKey[next_node] if next_node != '' else None - - def get_index(self, item): - return self.TKTreeview.index(self.KeyToID[item]) - - def move_up(self, item): - index = self.get_index(item) - if index > 0: - index -= 1 - parent = self.TKTreeview.parent(self.KeyToID[item]) - self.TKTreeview.move(self.KeyToID[item], parent, index) - - def move_down(self, item): - index = self.get_index(item) + 1 - parent = self.TKTreeview.parent(self.KeyToID[item]) - self.TKTreeview.move(self.KeyToID[item], parent, index) - - def selection_set(self, items): - ids = [self.KeyToID[item] for item in items] - self.TKTreeview.selection_set(ids) - - def sort_alphabetically(self): - nodes_list = self.TreeData.get_node_data_by_key('')['children'] - nodes = [(node.key, node.text) for node in nodes_list] - nodes.sort(key=lambda tup: tup[1]) - for idx, item in enumerate(nodes): - self.TKTreeview.move(self.KeyToID[item[0]], '', idx) - - def remove_node_and_select_nearest(self, node): - self.TreeData.remove_node(node) - prev_node = self.get_previous_sibling(node, parent_if_none=True) - self.Update(values=self.TreeData) - self.see(prev_node) - self.selection_set([prev_node]) - - def add_node_to_root_with_children(self, node, children=None): - if children is None: - children = [] - parent_node = self.TreeData.Insert('', key=str(uuid4()), text=node.text, values=node.values, icon=node.icon) - for kid in children: - self.TreeData.Insert(parent_node, key=str(uuid4()), text=kid.text, values=kid.values, icon=kid.icon) - self.Update(values=self.TreeData) - self.see(parent_node) - return parent_node - - def update_node(self, key, text, values=None, icon=None): - node_data = self.TreeData.get_node_data_by_key(key) - if not values: - values = node_data['values'] - if not icon: - icon = node_data['icon'] - self.TreeData.update_node(key, text, values, icon) - selection_before_update = self.get_selection() - self.Update(values=self.TreeData) - self.see(key) - self.selection_set(selection_before_update) - - def get_last_of_family_nodes(self): - last_of_family = [] - for key in self.TreeData.tree_dict.keys(): - node = self.TreeData.get_node_data_by_key(key) - if not node['children'] and not self.get_next_sibling(key): - last_of_family.append(key) - return last_of_family diff --git a/core/TreeNode.py b/core/TreeNode.py deleted file mode 100644 index e7e78b7..0000000 --- a/core/TreeNode.py +++ /dev/null @@ -1,6 +0,0 @@ -class TreeNode(object): - - def __init__(self, text, values, icon=None): - self.text = text - self.values = values - self.icon = icon diff --git a/forms/AddExampleForm.py b/forms/AddExampleForm.py deleted file mode 100644 index d9a1496..0000000 --- a/forms/AddExampleForm.py +++ /dev/null @@ -1,59 +0,0 @@ -from uuid import uuid4 - -import PySimpleGUI as sg - -from common.constants import * -from gui.layout import button_params - - -class AddExampleForm(object): - FORM_NAME = 'Add example' - INPUT_LABEL = 'Example' - EMPTY_TEXT_ERROR = "Text cannot be empty." - INPUT_KEY = 'example_text' - - def __init__(self, return_to, parent_key, parent_type, handler): - self.return_to = return_to - self.parent_key = parent_key - self.parent_type = parent_type - self.handler = handler - self.form = sg.FlexForm(self.FORM_NAME, return_keyboard_events=True).layout(self.layout()) - self.icon = QUESTION_ICON if self.parent_type == TYPE_INTENT else ANSWER_ICON - - def layout(self): - layout = [ - [sg.Text(self.INPUT_LABEL), sg.InputText(key=self.INPUT_KEY)], - [sg.Button(ACTION_SUBMIT, **button_params(green_button)), - sg.Button(ACTION_CANCEL, **button_params(orange_button))] - ] - return layout - - def validate(self, values): - error = None - if values[self.INPUT_KEY] == '': - error = self.EMPTY_TEXT_ERROR - return error - - def process(self): - self.return_to.Disable() - new_item = None - while True: - button, values = self.form.Read() - if button in DEFAULT_FORM_SUBMIT_ACTIONS: - error = self.validate(values) - if not error: - new_item = self.handler.tree.TreeData.Insert(self.parent_key, key=str(uuid4()), text=values[self.INPUT_KEY], values=[], icon=self.icon) - self.handler.tree.Update(values=self.handler.tree.TreeData) - self.handler.tree.see(new_item) - self.handler.tree.selection_set([new_item]) - else: - sg.Popup(error, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, keep_on_top=True) - self.form.close() - break - elif button in DEFAULT_FORM_CANCEL_ACTIONS: - self.form.close() - break - self.handler.sort_alphabetically() - self.return_to.bring_to_front() - self.return_to.Enable() - return new_item diff --git a/forms/AddItemForm.py b/forms/AddItemForm.py deleted file mode 100644 index 6c01154..0000000 --- a/forms/AddItemForm.py +++ /dev/null @@ -1,74 +0,0 @@ -import PySimpleGUI as sg - -from common.constants import * -from core.TreeNode import TreeNode -from gui.layout import button_params - - -class AddItemForm(object): - INPUT_EXAMPLES_LABEL = "Text examples (may fill blank some)" - EMPTY_NAME_ERROR = "Name cannot be empty." - NO_EXAMPLES_ERROR = "There should be at least one example." - NON_UNIQUE_NAME_ERROR = "Name is not unique." - INPUT_KEY_NAME = 'item_name' - - def __init__(self, return_to, form_name, item_type, handler): - self.return_to = return_to - self.item_type = item_type - self.handler = handler - self.form = sg.FlexForm(form_name, return_keyboard_events=True).layout(self.layout()) - self.parent_icon = ANSWER_ICON - self.children_icon = QUESTION_ICON if self.item_type == TYPE_INTENT else ANSWER_ICON - - def layout(self): - layout = [ - [sg.Text(self.item_type), sg.InputText(key=self.INPUT_KEY_NAME)], - [sg.Text(self.INPUT_EXAMPLES_LABEL)], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.InputText()], - [sg.Button(ACTION_SUBMIT, **button_params(green_button)), - sg.Button(ACTION_CANCEL, **button_params(orange_button))] - ] - return layout - - def validate(self, values): - error = None - if values[self.INPUT_KEY_NAME] == '': - error = self.EMPTY_NAME_ERROR - elif all([v == '' for k, v in values.items() if k != self.INPUT_KEY_NAME]): - error = self.NO_EXAMPLES_ERROR - elif values[self.INPUT_KEY_NAME] in self.handler.items.keys(): - error = self.NON_UNIQUE_NAME_ERROR - return error - - def process(self): - self.return_to.Disable() - new_item = None - while True: - button, values = self.form.Read() - if button in DEFAULT_FORM_SUBMIT_ACTIONS: - error = self.validate(values) - if not error: - parent_node = TreeNode(text=values[self.INPUT_KEY_NAME], values=[], icon=self.parent_icon) - kids = [TreeNode(text=value, values=[], icon=self.children_icon) for value in [v for k, v in values.items() if k != self.INPUT_KEY_NAME and v != '']] - new_item = self.handler.tree.add_node_to_root_with_children(parent_node, kids) - self.handler.items[values[self.INPUT_KEY_NAME]] = new_item - else: - sg.Popup(error, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, keep_on_top=True) - self.form.close() - break - elif button in DEFAULT_FORM_CANCEL_ACTIONS: - self.form.close() - break - self.handler.sort_alphabetically() - self.return_to.bring_to_front() - self.return_to.Enable() - return new_item diff --git a/forms/AddStoryItemForm.py b/forms/AddStoryItemForm.py deleted file mode 100644 index 872c142..0000000 --- a/forms/AddStoryItemForm.py +++ /dev/null @@ -1,46 +0,0 @@ -import PySimpleGUI as sg - -from common.constants import * -from gui.layout import button_params - - -class AddStoryItemForm(object): - INPUT_LABEL = "Item" - INPUT_KEY_NAME = 'add_item' - - def __init__(self, return_to, form_name, parent_object_key, stories): - self.return_to = return_to - self.stories = stories - self.parent_object_key = parent_object_key - self.parent_object_type = stories.tree_data.get_node_data_by_key(parent_object_key)['values'][0] if parent_object_key != '' else 'root' - available_values = None - if self.parent_object_type == TYPE_INTENT: - available_values = sorted(self.stories.resp.items.keys()) - elif self.parent_object_type in (TYPE_RESPONSE, 'root'): - available_values = sorted(self.stories.nlu.items.keys()) - self.form = sg.FlexForm(form_name, return_keyboard_events=True).layout(self.layout(available_values)) - - def layout(self, available_values): - layout = [ - [sg.Text(self.INPUT_LABEL), sg.Combo(size=(50, 30), key=self.INPUT_KEY_NAME, font='Any 12', values=tuple(available_values))], - [sg.Button(ACTION_SUBMIT, **button_params(green_button)), - sg.Button(ACTION_CANCEL, **button_params(orange_button))] - ] - return layout - - def process(self): - self.return_to.Disable() - new_item = None - while True: - button, values = self.form.Read() - if button in DEFAULT_FORM_SUBMIT_ACTIONS: - self.stories.add_item(parent_key=self.parent_object_key, parent_type=self.parent_object_type, text=values[self.INPUT_KEY_NAME]) - self.form.close() - break - elif button in DEFAULT_FORM_CANCEL_ACTIONS: - self.form.close() - break - self.stories.sort_alphabetically() - self.return_to.bring_to_front() - self.return_to.Enable() - return new_item diff --git a/forms/RemoveItemForm.py b/forms/RemoveItemForm.py deleted file mode 100644 index 7b6a302..0000000 --- a/forms/RemoveItemForm.py +++ /dev/null @@ -1,34 +0,0 @@ -import PySimpleGUI as sg - - -class RemoveItemForm(object): - USED_ITEM_ERROR = "This answer is used somewhere in stories, so can't remove it." - - def __init__(self, item_key, item_type, handler, stories): - self.handler = handler - self.item_key = item_key - self.item_text = self.handler.tree_data.get_node_data_by_key(self.item_key)['text'] - self.item_type = item_type - self.stories = stories - - def layout(self): - return None - - def validate(self): - error = None - for item in self.stories.tree_data.tree_dict.keys(): - data = self.stories.tree_data.get_node_data_by_key(item) - if data['values'] and data['values'][0] == self.item_type and data['text'] == self.item_text: - error = self.USED_ITEM_ERROR - return error - - def process(self): - error = self.validate() - if not error: - self.handler.tree.remove_node_and_select_nearest(self.item_key) - if self.item_text in self.handler.items: - del self.handler.items[self.item_text] - self.handler.sort_alphabetically() - else: - sg.Popup(error, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, keep_on_top=True, - button_type=sg.POPUP_BUTTONS_NO_BUTTONS) diff --git a/forms/UpdateItemForm.py b/forms/UpdateItemForm.py deleted file mode 100644 index 1ee4b6d..0000000 --- a/forms/UpdateItemForm.py +++ /dev/null @@ -1,53 +0,0 @@ -import PySimpleGUI as sg - -from common.constants import * -from gui.layout import button_params - - -class UpdateItemForm(object): - EMPTY_TEXT_ERROR = "Text cannot be empty." - INPUT_KEY = 'updated_text' - - def __init__(self, return_to, form_name, updated_item_key, updated_item_type, handler, stories): - self.return_to = return_to - self.updated_item_key = updated_item_key - self.updated_item_type = updated_item_type - self.handler = handler - self.stories = stories - self.form = sg.FlexForm(form_name, return_keyboard_events=True) - - def layout(self, old_text): - update_text_layout = [ - [sg.InputText(key=self.INPUT_KEY, default_text=old_text)], - [sg.Button(ACTION_SUBMIT, **button_params(green_button)), - sg.Button(ACTION_CANCEL, **button_params(orange_button))] - ] - return update_text_layout - - def process(self): - self.return_to.Disable() - node_data = self.handler.tree_data.get_node_data_by_key(self.updated_item_key) - self.form.Layout(self.layout(node_data['text'])) - - while True: - button, values = self.form.Read() - if button in DEFAULT_FORM_SUBMIT_ACTIONS: - if values[self.INPUT_KEY] == '': - sg.Popup(self.EMPTY_TEXT_ERROR, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, keep_on_top=True) - else: - self.handler.tree.update_node(self.updated_item_key, values['updated_text']) - if node_data['text'] in self.handler.items: - del self.handler.items[node_data['text']] - self.handler.items[values[self.INPUT_KEY]] = self.updated_item_key - for item in self.stories.tree_data.tree_dict.keys(): - data = self.stories.tree_data.get_node_data_by_key(item) - if data['values'] and data['values'][0] == self.updated_item_type and data['text'] == node_data['text']: - self.stories.tree.update_node(item, values['updated_text']) - self.form.close() - break - elif button in DEFAULT_FORM_CANCEL_ACTIONS: - self.form.close() - break - self.handler.sort_alphabetically() - self.return_to.bring_to_front() - self.return_to.Enable() diff --git a/gui/core/ExtendedTree.py b/gui/core/ExtendedTree.py new file mode 100644 index 0000000..bf4cb4b --- /dev/null +++ b/gui/core/ExtendedTree.py @@ -0,0 +1,28 @@ +import PySimpleGUI as sg + + +class ExtendedTree(sg.Tree): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def see(self, item): + id = self.KeyToID[item] + self.TKTreeview.see(id) + + def get_selection(self): + return [self.IdToKey[item] for item in self.TKTreeview.selection()] + + def get_parent(self, item): + return self.IdToKey[self.TKTreeview.parent(self.KeyToID[item])] + + def get_previous_sibling(self, item, parent_if_none=False): + prev = self.TKTreeview.prev(self.KeyToID[item]) + if prev == "": + result = self.get_parent(item) if parent_if_none else None + else: + result = self.IdToKey[prev] + return result + + def selection_set(self, items): + ids = [self.KeyToID[item] for item in items] + self.TKTreeview.selection_set(ids) diff --git a/handlers/__init__.py b/gui/core/__init__.py similarity index 100% rename from handlers/__init__.py rename to gui/core/__init__.py diff --git a/gui/forms/AddExampleForm.py b/gui/forms/AddExampleForm.py new file mode 100644 index 0000000..d08c980 --- /dev/null +++ b/gui/forms/AddExampleForm.py @@ -0,0 +1,67 @@ +import PySimpleGUI as sg + +from common.constants import * +from gui.layout import button_params + + +class AddExampleForm(object): + FORM_NAME = "Add example" + INPUT_LABEL = "Example" + EMPTY_TEXT_ERROR = "Text cannot be empty." + ITEM_WITH_KEY_EXISTS_ERROR = "Item with such name/text already exists." + INPUT_KEY = "example_text" + + def __init__(self, return_to, parent_name, handler, tree): + self.return_to = return_to + self.parent_name = parent_name + self.handler = handler + self.tree = tree + self.form = sg.FlexForm(self.FORM_NAME, return_keyboard_events=True).layout( + self.layout() + ) + + def layout(self): + layout = [ + [sg.Text(self.INPUT_LABEL), sg.InputText(key=self.INPUT_KEY)], + [ + sg.Button(ACTION_SUBMIT, **button_params(green_button)), + sg.Button(ACTION_CANCEL, **button_params(orange_button)), + ], + ] + return layout + + def validate(self, values): + error = None + if values[self.INPUT_KEY] == "": + error = self.EMPTY_TEXT_ERROR + if values[self.INPUT_KEY] in self.handler.items: + error = self.ITEM_WITH_KEY_EXISTS_ERROR + return error + + def process(self): + self.return_to.Disable() + new_item = None + while True: + button, values = self.form.Read() + if button in DEFAULT_FORM_SUBMIT_ACTIONS: + error = self.validate(values) + if not error: + new_item = values[self.INPUT_KEY] + self.handler.add_example_to_node(self.parent_name, new_item) + self.tree.Update(values=self.handler.export_to_pysg_tree()) + self.tree.see(new_item) + self.tree.selection_set([new_item]) + else: + sg.Popup( + error, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, + keep_on_top=True, + ) + self.form.close() + break + elif button in DEFAULT_FORM_CANCEL_ACTIONS: + self.form.close() + break + self.return_to.bring_to_front() + self.return_to.Enable() + return new_item diff --git a/gui/forms/AddItemForm.py b/gui/forms/AddItemForm.py new file mode 100644 index 0000000..42a7826 --- /dev/null +++ b/gui/forms/AddItemForm.py @@ -0,0 +1,97 @@ +import PySimpleGUI as sg + +from common.constants import * +from gui.layout import button_params + + +class AddItemForm(object): + INPUT_EXAMPLES_LABEL = "Text examples (may fill blank some)" + EMPTY_NAME_ERROR = "Name cannot be empty." + NO_EXAMPLES_ERROR = "There should be at least one example." + INPUT_KEY_NAME = "item_name" + ITEM_WITH_KEY_EXISTS_ERROR = "Item with such name/example text already exists. All elements in a tree should be unique." + ITEM_NAME_IS_DUPLICATED_IN_EXAMPLES = "Item name is duplicated in one of examples" + THERE_ARE_DUPLICATES_IN_EXAMPLES = ( + "There are duplicates in examples. Examples should be unique." + ) + + def __init__(self, return_to, form_name, item_type, handler, tree): + self.return_to = return_to + self.item_type = item_type + self.handler = handler + self.form = sg.FlexForm(form_name, return_keyboard_events=True).layout( + self.layout() + ) + self.tree = tree + + def layout(self): + layout = [ + [sg.Text(self.item_type), sg.InputText(key=self.INPUT_KEY_NAME)], + [sg.Text(self.INPUT_EXAMPLES_LABEL)], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [sg.InputText()], + [ + sg.Button(ACTION_SUBMIT, **button_params(green_button)), + sg.Button(ACTION_CANCEL, **button_params(orange_button)), + ], + ] + return layout + + def validate(self, new_item_name, children_texts): + error = None + if new_item_name == "": + error = self.EMPTY_NAME_ERROR + elif all([v == "" for v in children_texts]): + error = self.NO_EXAMPLES_ERROR + elif new_item_name in self.handler.items: + error = self.ITEM_WITH_KEY_EXISTS_ERROR + elif any([v in self.handler.items for v in children_texts]): + error = self.ITEM_WITH_KEY_EXISTS_ERROR + elif new_item_name in children_texts: + error = self.ITEM_NAME_IS_DUPLICATED_IN_EXAMPLES + elif len(set(children_texts)) != len(children_texts): + error = self.THERE_ARE_DUPLICATES_IN_EXAMPLES + return error + + def process(self): + self.return_to.Disable() + while True: + button, values = self.form.Read() + new_item = values[self.INPUT_KEY_NAME] + children_texts = [ + value + for value in [ + v for k, v in values.items() if k != self.INPUT_KEY_NAME and v != "" + ] + ] + if button in DEFAULT_FORM_SUBMIT_ACTIONS: + error = self.validate(new_item, children_texts) + if not error: + self.handler.add_node_with_kids( + values[self.INPUT_KEY_NAME], *children_texts + ) + self.tree.Update(self.handler.export_to_pysg_tree()) + self.tree.see(new_item) + self.tree.selection_set([new_item]) + else: + sg.Popup( + error, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, + keep_on_top=True, + ) + self.form.close() + break + elif button in DEFAULT_FORM_CANCEL_ACTIONS: + self.form.close() + break + self.return_to.bring_to_front() + self.return_to.Enable() + return new_item diff --git a/gui/forms/AddStoryItemForm.py b/gui/forms/AddStoryItemForm.py new file mode 100644 index 0000000..3e0e296 --- /dev/null +++ b/gui/forms/AddStoryItemForm.py @@ -0,0 +1,61 @@ +import PySimpleGUI as sg + +from common.constants import * +from gui.layout import button_params + + +class AddStoryItemForm(object): + INPUT_LABEL = "Item" + INPUT_KEY_NAME = "add_item" + + def __init__(self, return_to, form_name, parent_object_key, stories, tree): + self.return_to = return_to + self.stories = stories + self.tree = tree + self.parent_object_key = parent_object_key + self.available_values = self.stories.get_available_children_by_parent_id( + parent_object_key + ) + self.form = sg.FlexForm(form_name, return_keyboard_events=True).layout( + self.layout(self.available_values) + ) + + def layout(self, available_values): + layout = [ + [ + sg.Text(self.INPUT_LABEL), + sg.Combo( + size=(50, 30), + key=self.INPUT_KEY_NAME, + font="Any 12", + values=tuple(available_values), + ), + ], + [ + sg.Button(ACTION_SUBMIT, **button_params(green_button)), + sg.Button(ACTION_CANCEL, **button_params(orange_button)), + ], + ] + return layout + + def process(self): + self.return_to.Disable() + new_item = None + while True: + button, values = self.form.Read() + if button in DEFAULT_FORM_SUBMIT_ACTIONS: + new_item = self.stories.add_item( + parent_object_id=self.parent_object_key, + text=values[self.INPUT_KEY_NAME], + ) + self.tree.Update(self.stories.export_to_pysg_tree()) + self.tree.see(new_item.id) + self.tree.selection_set([new_item.id]) + self.form.close() + break + elif button in DEFAULT_FORM_CANCEL_ACTIONS: + self.form.close() + break + self.return_to.bring_to_front() + self.return_to.Enable() + return new_item diff --git a/gui/forms/RemoveItemForm.py b/gui/forms/RemoveItemForm.py new file mode 100644 index 0000000..a8707ec --- /dev/null +++ b/gui/forms/RemoveItemForm.py @@ -0,0 +1,33 @@ +import PySimpleGUI as sg + + +class RemoveItemForm(object): + USED_ITEM_ERROR = "This answer is used somewhere in stories, so can't remove it." + + def __init__(self, item_key, item_type, handler, stories, tree): + self.handler = handler + self.item_key = item_key + self.item_type = item_type + self.stories = stories + self.tree = tree + + def layout(self): + return None + + def process(self): + try: + prev_node = self.tree.get_previous_sibling( + self.item_key, parent_if_none=True + ) + self.handler.remove_node(self.item_key) + except ValueError as e: + sg.Popup( + e, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, + keep_on_top=True, + button_type=sg.POPUP_BUTTONS_NO_BUTTONS, + ) + else: + self.tree.Update(self.handler.export_to_pysg_tree()) + self.tree.see(prev_node) + self.tree.selection_set([prev_node]) diff --git a/gui/forms/RemoveStoryItemForm.py b/gui/forms/RemoveStoryItemForm.py new file mode 100644 index 0000000..59058f3 --- /dev/null +++ b/gui/forms/RemoveStoryItemForm.py @@ -0,0 +1,18 @@ +from backend.handlers import StoriesHandler + + +class RemoveStoryItemForm(object): + def __init__(self, item_key, stories: StoriesHandler, tree): + self.item_key = item_key + self.stories = stories + self.tree = tree + + def layout(self): + pass + + def process(self): + prev_node = self.tree.get_previous_sibling(self.item_key, parent_if_none=True) + self.stories.remove_item(self.item_key) + self.tree.Update(self.stories.export_to_pysg_tree()) + self.tree.see(prev_node) + self.tree.selection_set([prev_node]) diff --git a/gui/forms/UpdateItemForm.py b/gui/forms/UpdateItemForm.py new file mode 100644 index 0000000..1f8304c --- /dev/null +++ b/gui/forms/UpdateItemForm.py @@ -0,0 +1,78 @@ +import PySimpleGUI as sg + +from common.constants import * +from gui.layout import button_params + + +class UpdateItemForm(object): + EMPTY_TEXT_ERROR = "Text cannot be empty." + INPUT_KEY = "updated_text" + ITEM_WITH_KEY_EXISTS_ERROR = "Item with such name/text already exists." + + def __init__( + self, + return_to, + form_name, + updated_item_key, + updated_item_type, + handler, + tree, + stories, + stories_tree, + ): + self.return_to = return_to + self.updated_item_key = updated_item_key + self.updated_item_type = updated_item_type + self.handler = handler + self.stories = stories + self.tree = tree + self.stories_tree = stories_tree + self.form = sg.FlexForm(form_name, return_keyboard_events=True) + + def layout(self, old_text): + update_text_layout = [ + [sg.InputText(key=self.INPUT_KEY, default_text=old_text)], + [ + sg.Button(ACTION_SUBMIT, **button_params(green_button)), + sg.Button(ACTION_CANCEL, **button_params(orange_button)), + ], + ] + return update_text_layout + + def validate(self, values): + error = None + if values[self.INPUT_KEY] == "": + error = self.EMPTY_TEXT_ERROR + elif values[self.INPUT_KEY] in self.handler.items: + error = self.ITEM_WITH_KEY_EXISTS_ERROR + return error + + def process(self): + self.return_to.Disable() + self.form.Layout(self.layout(self.updated_item_key)) + + while True: + button, values = self.form.Read() + if button in DEFAULT_FORM_SUBMIT_ACTIONS: + error = self.validate(values) + if not error: + self.handler.update_node_value( + self.updated_item_key, values["updated_text"] + ) + self.tree.Update(self.handler.export_to_pysg_tree()) + self.tree.see(values["updated_text"]) + self.tree.selection_set([values["updated_text"]]) + self.stories_tree.Update(self.stories.export_to_pysg_tree()) + else: + sg.Popup( + error, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_CRITICAL, + keep_on_top=True, + ) + self.form.close() + break + elif button in DEFAULT_FORM_CANCEL_ACTIONS: + self.form.close() + break + self.return_to.bring_to_front() + self.return_to.Enable() diff --git a/gui/forms/__init__.py b/gui/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/layout.py b/gui/layout.py index 7f43c49..d18d053 100644 --- a/gui/layout.py +++ b/gui/layout.py @@ -7,8 +7,9 @@ from common.constants import * sg.theme(APP_THEME) -sg.set_options(auto_size_buttons=True, border_width=0, - button_color=sg.COLOR_SYSTEM_DEFAULT) +sg.set_options( + auto_size_buttons=True, border_width=0, button_color=sg.COLOR_SYSTEM_DEFAULT +) wcolor = (APP_THEME_BG_COLOR, APP_THEME_BG_COLOR) @@ -17,56 +18,99 @@ def image_file_to_bytes(image64, size): img = Image.open(image_file) img.thumbnail(size, Image.ANTIALIAS) bio = io.BytesIO() - img.save(bio, format='PNG') + img.save(bio, format="PNG") imgbytes = bio.getvalue() return imgbytes def button_params(button_image): - button_params = {"image_data": image_file_to_bytes(button_image, (110, 50)), - "button_color": wcolor, - "font": "Any 11", - "border_width": 0} + button_params = { + "image_data": image_file_to_bytes(button_image, (110, 50)), + "button_color": wcolor, + "font": "Any 11", + "border_width": 0, + } return button_params -def generate_main_window_layout(nlu_tree, response_tree, stories_tree): - intent_tab_layout = [[sg.Text(TAB_INTENTS_DESCRIPTION)], - [nlu_tree], - [sg.Button(ACTION_ADD_INTENT, **button_params(green_button)), - sg.Button(BUTTON_ADD_EXAMPLE, key=ACTION_ADD_INTENT_EXAMPLE, **button_params(black_button)), - sg.Button(ACTION_UPDATE_INTENT, **button_params(blue_button)), - sg.Button(ACTION_REMOVE_INTENT, **button_params(orange_button))]] +def generate_main_window_layout(nlu_tree_data, response_tree, stories_tree): + intent_tab_layout = [ + [sg.Text(TAB_INTENTS_DESCRIPTION)], + [nlu_tree_data], + [ + sg.Button(ACTION_ADD_INTENT, **button_params(green_button)), + sg.Button( + BUTTON_ADD_EXAMPLE, + key=ACTION_ADD_INTENT_EXAMPLE, + **button_params(black_button) + ), + sg.Button(ACTION_UPDATE_INTENT, **button_params(blue_button)), + sg.Button(ACTION_REMOVE_INTENT, **button_params(orange_button)), + ], + ] - response_tab_layout = [[sg.Text(TAB_RESPONSES_DESCRIPTION)], - [response_tree], - [sg.Button(ACTION_ADD_RESPONSE, **button_params(green_button)), - sg.Button(BUTTON_ADD_EXAMPLE, key=ACTION_ADD_RESPONSE_EXAMPLE, **button_params(black_button)), - sg.Button(ACTION_UPDATE_RESPONSE, **button_params(blue_button)), - sg.Button(ACTION_REMOVE_RESPONSE, **button_params(orange_button))]] + response_tab_layout = [ + [sg.Text(TAB_RESPONSES_DESCRIPTION)], + [response_tree], + [ + sg.Button(ACTION_ADD_RESPONSE, **button_params(green_button)), + sg.Button( + BUTTON_ADD_EXAMPLE, + key=ACTION_ADD_RESPONSE_EXAMPLE, + **button_params(black_button) + ), + sg.Button(ACTION_UPDATE_RESPONSE, **button_params(blue_button)), + sg.Button(ACTION_REMOVE_RESPONSE, **button_params(orange_button)), + ], + ] - stories_tab_layout = [[sg.Text(TAB_STORIES_DESCRIPTION)], - [stories_tree], - [sg.Button(ACTION_ADD_CHILD, **button_params(green_button)), - sg.Button(ACTION_ADD_SIBLING, **button_params(green_button)), - sg.Button(ACTION_REMOVE_STORY_ITEM, **button_params(orange_button)), - sg.Button(ACTION_MOVE_STORY_ITEM_UP, **button_params(black_button)), - sg.Button(ACTION_MOVE_STORY_ITEM_DOWN, **button_params(black_button))]] + stories_tab_layout = [ + [sg.Text(TAB_STORIES_DESCRIPTION)], + [stories_tree], + [ + sg.Button(ACTION_ADD_CHILD, **button_params(green_button)), + sg.Button(ACTION_ADD_SIBLING, **button_params(green_button)), + sg.Button(ACTION_REMOVE_STORY_ITEM, **button_params(orange_button)), + ], + ] - main_window_layout = [[sg.TabGroup([[sg.Tab(TAB_INTENTS_HEADING, intent_tab_layout), - sg.Tab(TAB_RESPONSES_HEADING, response_tab_layout), - sg.Tab(TAB_STORIES_HEADING, stories_tab_layout)]])], - [sg.Button(ACTION_EXPORT_DATA, **button_params(blue_button)), - sg.Button(ACTION_CLOSE_WINDOW, **button_params(orange_button))]] + main_window_layout = [ + [ + sg.TabGroup( + [ + [ + sg.Tab(TAB_INTENTS_HEADING, intent_tab_layout), + sg.Tab(TAB_RESPONSES_HEADING, response_tab_layout), + sg.Tab(TAB_STORIES_HEADING, stories_tab_layout), + ] + ] + ) + ], + [ + sg.Button(ACTION_EXPORT_DATA, **button_params(blue_button)), + sg.Button(ACTION_CLOSE_WINDOW, **button_params(orange_button)), + ], + ] return main_window_layout def generate_import_window_layout(): add_intent_layout = [ - [sg.Text(LOCATE_FILE_TEXT.format(filename="nlu.md")), sg.FileBrowse(key=NLU_FILE_KEY, file_types=(("nlu", "*.md"),))], - [sg.Text(LOCATE_FILE_TEXT.format(filename="domain.yml")), sg.FileBrowse(key=DOMAIN_FILE_KEY, file_types=(("domain", "*.yml"),))], - [sg.Text(LOCATE_FILE_TEXT.format(filename="stories.md")), sg.FileBrowse(key=STORIES_FILE_KEY, file_types=(("stories", ".md"),))], - [sg.Button(ACTION_SUBMIT, **button_params(green_button)), - sg.Button(ACTION_CANCEL, **button_params(orange_button))] + [ + sg.Text(LOCATE_FILE_TEXT.format(filename="nlu.md / nlu.json")), + sg.FileBrowse(key=NLU_FILE_KEY, file_types=(("nlu", "*.*"),)), + ], + [ + sg.Text(LOCATE_FILE_TEXT.format(filename="domain.yml")), + sg.FileBrowse(key=DOMAIN_FILE_KEY, file_types=(("domain", "*.yml"),)), + ], + [ + sg.Text(LOCATE_FILE_TEXT.format(filename="stories.md")), + sg.FileBrowse(key=STORIES_FILE_KEY, file_types=(("stories", ".md"),)), + ], + [ + sg.Button(ACTION_SUBMIT, **button_params(green_button)), + sg.Button(ACTION_CANCEL, **button_params(orange_button)), + ], ] return add_intent_layout diff --git a/gui/run.py b/gui/run.py index 7516986..71bfada 100644 --- a/gui/run.py +++ b/gui/run.py @@ -5,21 +5,25 @@ import PySimpleGUI as sg import gui.layout as lt +from backend.handlers.Exporter import Exporter +from backend.handlers.NLUHandler import NLUHandler +from backend.handlers.ResponseHandler import ResponseHandler +from backend.handlers.StoriesHandler import StoriesHandler from common.constants import * -from forms.AddItemForm import AddItemForm -from forms.UpdateItemForm import UpdateItemForm -from forms.AddExampleForm import AddExampleForm -from forms.AddStoryItemForm import AddStoryItemForm -from forms.RemoveItemForm import RemoveItemForm -from handlers.Exporter import Exporter -from handlers.NLUHandler import NLUHandler -from handlers.ResponseHandler import ResponseHandler -from handlers.StoriesHandler import StoriesHandler +from gui.core.ExtendedTree import ExtendedTree +from gui.forms.AddExampleForm import AddExampleForm +from gui.forms.AddItemForm import AddItemForm +from gui.forms.AddStoryItemForm import AddStoryItemForm +from gui.forms.RemoveItemForm import RemoveItemForm +from gui.forms.RemoveStoryItemForm import RemoveStoryItemForm +from gui.forms.UpdateItemForm import UpdateItemForm def launcher(): import_window_layout = lt.generate_import_window_layout() - import_window = sg.Window(LOCATE_FILES_WINDOW_NAME, import_window_layout, resizable=False) + import_window = sg.Window( + LOCATE_FILES_WINDOW_NAME, import_window_layout, resizable=False + ) nlu = resp = stories = None while True: @@ -35,90 +39,258 @@ def launcher(): stories = StoriesHandler(values[STORIES_FILE_KEY], nlu, resp) stories.import_data() except (FileNotFoundError, KeyError): - sg.Popup(MSG_INVALID_OR_UNEXISTING_FILE, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, keep_on_top=True, button_type=sg.POPUP_BUTTONS_NO_BUTTONS) + sg.Popup( + MSG_INVALID_OR_UNEXISTING_FILE, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, + keep_on_top=True, + button_type=sg.POPUP_BUTTONS_NO_BUTTONS, + ) if event in (None, ACTION_CLOSE_WINDOW): break import_window.close() - if nlu and resp and stories: - main_window_layout = lt.generate_main_window_layout(nlu.tree, resp.tree, stories.tree) + if nlu: # and resp and stories: + nlu_tree = ExtendedTree( + data=nlu.export_to_pysg_tree(), + headings=[], + col0_width=80, + key=INTENT_TREE_KEY, + right_click_menu=[ + "Right", + [ + "!Fast actions", + ACTION_ADD_INTENT, + ACTION_ADD_INTENT_EXAMPLE, + ACTION_UPDATE_INTENT, + ACTION_REMOVE_INTENT, + ], + ], + **TREE_LAYOUT_COMMON_PARAMS, + ) + resp_tree = ExtendedTree( + data=resp.export_to_pysg_tree(), + headings=[], + col0_width=80, + key=RESPONSE_TREE_KEY, + right_click_menu=[ + "Right", + [ + "!Fast actions", + ACTION_ADD_RESPONSE, + ACTION_ADD_RESPONSE_EXAMPLE, + ACTION_UPDATE_RESPONSE, + ACTION_REMOVE_RESPONSE, + ], + ], + **TREE_LAYOUT_COMMON_PARAMS, + ) + + stories_tree = ExtendedTree( + data=stories.export_to_pysg_tree(), + headings=["Type", "Text"], + col0_width=23, + col_widths=[7, 50], + key=STORIES_TREE_KEY, + right_click_menu=[ + "Right", + [ + "!Fast actions", + ACTION_ADD_CHILD, + ACTION_ADD_SIBLING, + ACTION_REMOVE_STORY_ITEM, + ACTION_ADD_NEW_ITEM_AS_A_CHILD, + ], + ], + **TREE_LAYOUT_COMMON_PARAMS, + ) + + main_window_layout = lt.generate_main_window_layout( + nlu_tree, resp_tree, stories_tree + ) main_window = sg.Window(APP_NAME, main_window_layout, resizable=False) main_window.read() - nlu.sort_alphabetically() - resp.sort_alphabetically() - stories.sort_alphabetically() while True: event, values = main_window.read() if event == ACTION_ADD_INTENT: - AddItemForm(return_to=main_window, form_name=FORM_NAME_ADD_INTENT, item_type=TYPE_INTENT, handler=nlu).process() + AddItemForm( + return_to=main_window, + form_name=FORM_NAME_ADD_INTENT, + item_type=TYPE_INTENT, + handler=nlu, + tree=nlu_tree, + ).process() if event == ACTION_ADD_RESPONSE: - AddItemForm(return_to=main_window, form_name=FORM_NAME_ADD_RESPONSE, item_type=TYPE_RESPONSE, handler=resp).process() + AddItemForm( + return_to=main_window, + form_name=FORM_NAME_ADD_RESPONSE, + item_type=TYPE_RESPONSE, + handler=resp, + tree=resp_tree, + ).process() if event == ACTION_ADD_INTENT_EXAMPLE: - AddExampleForm(return_to=main_window, parent_key=values[INTENT_TREE_KEY][0], parent_type=TYPE_INTENT, handler=nlu).process() + AddExampleForm( + return_to=main_window, + parent_name=values[INTENT_TREE_KEY][0], + handler=nlu, + tree=nlu_tree, + ).process() if event == ACTION_ADD_RESPONSE_EXAMPLE: - AddExampleForm(return_to=main_window, parent_key=values[RESPONSE_TREE_KEY][0], parent_type=TYPE_RESPONSE, handler=resp).process() + AddExampleForm( + return_to=main_window, + parent_name=values[RESPONSE_TREE_KEY][0], + handler=resp, + tree=resp_tree, + ).process() if event == ACTION_UPDATE_INTENT and values[INTENT_TREE_KEY]: - UpdateItemForm(return_to=main_window, form_name=FORM_NAME_EDIT_INTENT, updated_item_key=values[INTENT_TREE_KEY][0], updated_item_type=TYPE_INTENT, handler=nlu, - stories=stories).process() + UpdateItemForm( + return_to=main_window, + form_name=FORM_NAME_EDIT_INTENT, + updated_item_key=values[INTENT_TREE_KEY][0], + updated_item_type=TYPE_INTENT, + handler=nlu, + tree=nlu_tree, + stories=stories, + stories_tree=stories_tree, + ).process() if event == ACTION_UPDATE_RESPONSE and values[RESPONSE_TREE_KEY]: - UpdateItemForm(return_to=main_window, form_name=FORM_NAME_EDIT_ANSWER, updated_item_key=values[RESPONSE_TREE_KEY][0], updated_item_type=TYPE_RESPONSE, handler=resp, - stories=stories).process() - - if event == ACTION_ADD_CHILD and values[STORIES_TREE_KEY]: - AddStoryItemForm(return_to=main_window, form_name=FORM_NAME_ADD_CHILD, parent_object_key=values[STORIES_TREE_KEY][0], stories=stories).process() - - if event == ACTION_ADD_SIBLING and values[STORIES_TREE_KEY]: - AddStoryItemForm(return_to=main_window, form_name=FORM_NAME_ADD_SIBLING, parent_object_key=stories.tree.get_parent(values[STORIES_TREE_KEY][0]), stories=stories).process() - - if event == ACTION_MOVE_STORY_ITEM_UP and values[STORIES_TREE_KEY]: - stories.tree.move_up(values[STORIES_TREE_KEY][0]) + UpdateItemForm( + return_to=main_window, + form_name=FORM_NAME_EDIT_ANSWER, + updated_item_key=values[RESPONSE_TREE_KEY][0], + updated_item_type=TYPE_RESPONSE, + handler=resp, + tree=resp_tree, + stories=stories, + stories_tree=stories_tree, + ).process() - if event == ACTION_MOVE_STORY_ITEM_DOWN and values[STORIES_TREE_KEY]: - stories.tree.move_down(values[STORIES_TREE_KEY][0]) + if event == ACTION_REMOVE_INTENT and values[INTENT_TREE_KEY]: + RemoveItemForm( + item_key=values[INTENT_TREE_KEY][0], + item_type=TYPE_INTENT, + handler=nlu, + stories=stories, + tree=nlu_tree, + ).process() if event == ACTION_REMOVE_RESPONSE and values[RESPONSE_TREE_KEY]: - RemoveItemForm(item_key=values[RESPONSE_TREE_KEY][0], item_type=TYPE_RESPONSE, handler=resp, stories=stories).process() + RemoveItemForm( + item_key=values[RESPONSE_TREE_KEY][0], + item_type=TYPE_RESPONSE, + handler=resp, + stories=stories, + tree=resp_tree, + ).process() - if event == ACTION_REMOVE_INTENT and values[INTENT_TREE_KEY]: - RemoveItemForm(item_key=values[INTENT_TREE_KEY][0], item_type=TYPE_INTENT, handler=nlu, stories=stories).process() + if event == ACTION_ADD_CHILD and values[STORIES_TREE_KEY]: + AddStoryItemForm( + return_to=main_window, + form_name=FORM_NAME_ADD_CHILD, + parent_object_key=values[STORIES_TREE_KEY][0], + stories=stories, + tree=stories_tree, + ).process() + + if event == ACTION_ADD_SIBLING and values[STORIES_TREE_KEY]: + parent_item = stories.get_parent_node_by_object_id( + values[STORIES_TREE_KEY][0] + ) + AddStoryItemForm( + return_to=main_window, + form_name=FORM_NAME_ADD_SIBLING, + parent_object_key=parent_item.id, + stories=stories, + tree=stories_tree, + ).process() if event == ACTION_REMOVE_STORY_ITEM and values[STORIES_TREE_KEY]: - stories.tree.remove_node_and_select_nearest(values[STORIES_TREE_KEY][0]) - stories.sort_alphabetically() + RemoveStoryItemForm( + item_key=values[STORIES_TREE_KEY][0], + stories=stories, + tree=stories_tree, + ).process() if event == ACTION_ADD_NEW_ITEM_AS_A_CHILD and values[STORIES_TREE_KEY]: parent_key = values[STORIES_TREE_KEY][0] - parent_type = stories.tree.TreeData.get_node_data_by_key(parent_key)['values'][0] - handler = nlu if parent_type == TYPE_RESPONSE else resp - item_type = TYPE_INTENT if parent_type == TYPE_RESPONSE else TYPE_RESPONSE - new_item = AddItemForm(return_to=main_window, form_name=FORM_NAME_ADD_STORY_ITEM, item_type=item_type, handler=handler).process() - if new_item: - stories.add_item(parent_key, parent_type, handler.tree_data.get_node_data_by_key(new_item)['text']) - - if event in (ACTION_UPDATE_RESPONSE, ACTION_REMOVE_RESPONSE, ACTION_ADD_RESPONSE_EXAMPLE) and not values[RESPONSE_TREE_KEY] \ - or event in (ACTION_UPDATE_INTENT, ACTION_REMOVE_INTENT, ACTION_ADD_INTENT_EXAMPLE) and not values[INTENT_TREE_KEY] \ - or event in (ACTION_ADD_CHILD, ACTION_ADD_SIBLING, ACTION_REMOVE_STORY_ITEM, ACTION_ADD_NEW_ITEM_AS_A_CHILD, - ACTION_MOVE_STORY_ITEM_UP, ACTION_MOVE_STORY_ITEM_DOWN) and not values[STORIES_TREE_KEY]: - sg.Popup(MSG_FIRST_SELECT_ITEM, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, keep_on_top=True, button_type=sg.POPUP_BUTTONS_NO_BUTTONS) + child_type, child_handler = stories.get_child_type_and_handler_by_id( + object_id=parent_key + ) + tree = nlu_tree if child_handler == nlu else resp_tree + child = AddItemForm( + return_to=main_window, + form_name=FORM_NAME_ADD_STORY_ITEM, + item_type=child_type, + handler=child_handler, + tree=tree, + ).process() + if child: + story_node = stories.add_item(parent_key, child) + stories_tree.Update(stories.export_to_pysg_tree()) + stories_tree.see(story_node.id) + stories_tree.selection_set([story_node.id]) + + if ( + event + in ( + ACTION_UPDATE_RESPONSE, + ACTION_REMOVE_RESPONSE, + ACTION_ADD_RESPONSE_EXAMPLE, + ) + and not values[RESPONSE_TREE_KEY] + or event + in ( + ACTION_UPDATE_INTENT, + ACTION_REMOVE_INTENT, + ACTION_ADD_INTENT_EXAMPLE, + ) + and not values[INTENT_TREE_KEY] + or event + in ( + ACTION_ADD_CHILD, + ACTION_ADD_SIBLING, + ACTION_REMOVE_STORY_ITEM, + ACTION_ADD_NEW_ITEM_AS_A_CHILD, + ) + and not values[STORIES_TREE_KEY] + ): + sg.Popup( + MSG_FIRST_SELECT_ITEM, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_WARNING, + keep_on_top=True, + button_type=sg.POPUP_BUTTONS_NO_BUTTONS, + ) if event == ACTION_EXPORT_DATA: Path(f"{os.getcwd()}/export").mkdir(exist_ok=True, parents=False) ts_suffix = int(time()) - exporter = Exporter(nlu, resp, stories, f"export/nlu_{ts_suffix}.md", f"export/domain_{ts_suffix}.yml", f"export/stories_{ts_suffix}.md") + exporter = Exporter( + nlu, + resp, + stories, + f"export/nlu_{ts_suffix}.md", + f"export/nlu_{ts_suffix}.json", + f"export/domain_{ts_suffix}.yml", + f"export/stories_{ts_suffix}.md", + ) exporter.export() - sg.Popup(MSG_EXPORT_SUCCESSFUL, icon=sg.SYSTEM_TRAY_MESSAGE_ICON_INFORMATION, keep_on_top=True, button_type=sg.POPUP_BUTTONS_NO_BUTTONS) + sg.Popup( + MSG_EXPORT_SUCCESSFUL, + icon=sg.SYSTEM_TRAY_MESSAGE_ICON_INFORMATION, + keep_on_top=True, + button_type=sg.POPUP_BUTTONS_NO_BUTTONS, + ) if event in (None, ACTION_CLOSE_WINDOW): break main_window.close() -if __name__ == '__main__': +if __name__ == "__main__": launcher() diff --git a/handlers/Exporter.py b/handlers/Exporter.py deleted file mode 100644 index f22bd88..0000000 --- a/handlers/Exporter.py +++ /dev/null @@ -1,33 +0,0 @@ -import shutil - -import yaml - - -class Exporter(object): - - def __init__(self, nlu, resp, stories, nlu_file, domain_file, stories_file): - self.nlu = nlu - self.resp = resp - self.stories = stories - self.nlu_file = nlu_file - self.domain_file = domain_file - self.stories_file = stories_file - - def export(self): - nlu_data = self.nlu.export_data() - resp_data = self.resp.export_data() - stories_data = self.stories.export_data() - - with open(self.nlu_file, 'w', encoding='utf-8') as nf: - nlu_data['result'].seek(0) - shutil.copyfileobj(nlu_data['result'], nf) - - domain_data = {"actions": resp_data['actions'], - "intents": nlu_data['intents'], - "responses": resp_data['responses']} - with open(self.domain_file, 'w', encoding='utf-8') as df: - df.write(yaml.safe_dump(domain_data, allow_unicode=True)) - - with open(self.stories_file, 'w', encoding='utf-8') as sf: - stories_data.seek(0) - shutil.copyfileobj(stories_data, sf) diff --git a/handlers/NLUHandler.py b/handlers/NLUHandler.py deleted file mode 100644 index fa9714e..0000000 --- a/handlers/NLUHandler.py +++ /dev/null @@ -1,56 +0,0 @@ -from io import StringIO # Python3 -from uuid import uuid4 - -import markdown_generator as mg - -from common.constants import * -from core.EditableTreeData import EditableTreeData -from core.ExtendedTree import ExtendedTree -from handlers.Handler import Handler - - -class NLUHandler(Handler): - - def __init__(self, filename, *args): - super().__init__(filename, *args) - self.filename = filename - self.items = {} - self.tree_data = EditableTreeData() - self.tree = ExtendedTree(data=self.tree_data, - headings=[], - col0_width=80, - key=INTENT_TREE_KEY, - right_click_menu=['Right', ['!Fast actions', - ACTION_ADD_INTENT, - ACTION_ADD_INTENT_EXAMPLE, - ACTION_UPDATE_INTENT, - ACTION_REMOVE_INTENT]], - **TREE_LAYOUT_COMMON_PARAMS) - - def import_data(self): - with open(self.filename, 'r', encoding='utf-8') as df: - nlu = df.readlines() - current_intent = None - for line in nlu: - if line.startswith("## intent:"): - heading = line.split("## intent:")[1].strip() - current_intent = self.tree_data.Insert('', key=str(uuid4()), text=heading, values=[], icon=ANSWER_ICON) - self.items[heading] = current_intent - if line.startswith("- "): - answer = line.split("- ")[1].strip() - self.tree_data.insert(current_intent, key=str(uuid4()), text=answer, values=[], icon=QUESTION_ICON) - - def export_data(self): - result = StringIO() - intents = [] - writer = mg.Writer(result) - for item in self.tree_data.dump(): - intents.append(item['text']) - writer.write_heading(f"intent:{item['text']}", 2) - examples = [f"- {str(kid['text'])}".strip() for kid in item['children']] - writer.writelines(examples) - return {"result": result, - "intents": intents} - - def sort_alphabetically(self): - self.tree.sort_alphabetically() diff --git a/handlers/ResponseHandler.py b/handlers/ResponseHandler.py deleted file mode 100644 index 36c0d5c..0000000 --- a/handlers/ResponseHandler.py +++ /dev/null @@ -1,54 +0,0 @@ -from uuid import uuid4 - -import yaml - - -from common.constants import * -from core.EditableTreeData import EditableTreeData -from core.ExtendedTree import ExtendedTree -from handlers.Handler import Handler - - -class ResponseHandler(Handler): - - def __init__(self, filename, *args): - super().__init__(filename, *args) - self.filename = filename - self.tree_data = EditableTreeData() - self.items = {} - - self.tree = ExtendedTree(data=self.tree_data, - headings=[], - col0_width=80, - key=RESPONSE_TREE_KEY, - right_click_menu=['Right', ['!Fast actions', - ACTION_ADD_RESPONSE, - ACTION_ADD_RESPONSE_EXAMPLE, - ACTION_UPDATE_RESPONSE, - ACTION_REMOVE_RESPONSE]], - **TREE_LAYOUT_COMMON_PARAMS - ) - - def import_data(self): - with open(self.filename, 'r', encoding='utf-8') as domain_file: - domain_data = yaml.safe_load(domain_file.read()) - for response, texts in domain_data['responses'].items(): - response_name = response.split('utter_')[-1].strip() - current_response = self.tree_data.Insert('', key=str(uuid4()), text=response_name, values=[], icon=ANSWER_ICON) - self.items[response_name] = current_response - for text in texts: - self.tree_data.Insert(current_response, key=str(uuid4()), text=text['text'], values=[], icon=ANSWER_ICON) - - def export_data(self): - responses = [] - result = {} - for item in self.tree_data.dump(): - responses.append(f"utter_{item['text']}") - result[f"utter_{item['text']}"] = [] - for kid in item['children']: - result[f"utter_{item['text']}"].append({"text": str(kid['text'].strip())}) - return {"responses": result, - "actions": responses} - - def sort_alphabetically(self): - self.tree.sort_alphabetically() diff --git a/handlers/StoriesHandler.py b/handlers/StoriesHandler.py deleted file mode 100644 index c1fe704..0000000 --- a/handlers/StoriesHandler.py +++ /dev/null @@ -1,98 +0,0 @@ -from io import StringIO -from uuid import uuid4 - -from common.constants import * -from core.EditableTreeData import EditableTreeData -from core.ExtendedTree import ExtendedTree -from handlers.Handler import Handler - - -class StoriesHandler(Handler): - - def __init__(self, filename, nlu, resp): - super().__init__(filename, nlu, resp) - self.filename = filename - self.response_list = [] - self.tree_data = EditableTreeData() - self.nlu = nlu - self.resp = resp - self.chains = {} - self.tree = ExtendedTree(data=self.tree_data, - headings=['Type', 'Text'], - col0_width=23, - col_widths=[7, 50], - key=STORIES_TREE_KEY, - right_click_menu=['Right', ['!Fast actions', - ACTION_ADD_CHILD, - ACTION_ADD_SIBLING, - ACTION_MOVE_STORY_ITEM_UP, - ACTION_MOVE_STORY_ITEM_DOWN, - ACTION_REMOVE_STORY_ITEM, - ACTION_ADD_NEW_ITEM_AS_A_CHILD]], - **TREE_LAYOUT_COMMON_PARAMS) - - def import_data(self): - - with open(self.filename, 'r', encoding='utf-8') as stories_file: - stories = stories_file.readlines() - for line in stories: - if line.startswith("## "): - current_answer = '' - current_intent = None - last_chain_indent = '' - last_chain_response = '' - - elif line.startswith("* "): - heading = line.split("* ")[-1].strip() - current_chain = str(last_chain_response + '-' + heading).strip("-") - if current_chain not in self.chains: - texts = " | ".join([child.text for child in self.nlu.tree_data.get_node_data_by_key(self.nlu.items[heading])['children']]) - current_intent = self.tree_data.Insert(current_answer, key=str(uuid4()), text=heading, values=[TYPE_INTENT, texts], icon=QUESTION_ICON) - self.chains[current_chain] = current_intent - else: - current_intent = self.chains[current_chain] - last_chain_indent = current_chain - - elif line.strip().startswith("- "): - heading = line.split("- ")[-1].strip() - current_chain = (last_chain_indent + '-' + heading).strip("-") - if current_chain not in self.chains: - texts = " | ".join([child.text for child in self.resp.tree_data.get_node_data_by_key(self.resp.items[heading.split("utter_")[-1]])['children']]) - current_answer = self.tree_data.Insert(current_intent, key=str(uuid4()), text=heading.split("utter_")[-1], values=[TYPE_RESPONSE, texts], icon=ANSWER_ICON) - self.chains[current_chain] = current_answer - else: - current_answer = self.chains[current_chain] - last_chain_response = current_chain - - def export_data(self): - result = StringIO() - for key in self.tree.get_last_of_family_nodes(): - story = self.tree.get_family_tree_in_story_format(key)[::-1] - story_heading = "-".join([item[0] for item in story if item[1] == TYPE_INTENT]) - result.write(f"\n\n## {story_heading}") - for item in story: - if item[1] == TYPE_INTENT: - result.write(f"\n* {item[0]}") - elif item[1] == TYPE_RESPONSE: - result.write(f"\n - utter_{item[0]}") - return result - - def add_item(self, parent_key, parent_type, text): - chain_value = (self.tree.get_family_tree(parent_key) + '-' + text).strip("-") - current_obj = '' - object_type = TYPE_INTENT if parent_type in (TYPE_RESPONSE, 'root') else TYPE_RESPONSE - handler = self.nlu if object_type == TYPE_INTENT else self.resp - icon = QUESTION_ICON if object_type == TYPE_INTENT else ANSWER_ICON - if chain_value in self.chains: - current_obj = self.chains[chain_value] - else: - object_texts = " | ".join([child.text for child in handler.tree_data.get_node_data_by_key(handler.items[text])['children']]) - current_obj = self.tree_data.Insert(parent_key, key=str(uuid4()), text=text, values=[object_type, object_texts], icon=QUESTION_ICON) - self.chains['chain_value'] = current_obj - self.tree.Update(values=self.tree_data) - self.tree.see(current_obj) - self.tree.selection_set([current_obj]) - return current_obj - - def sort_alphabetically(self): - self.tree.sort_alphabetically() diff --git a/requirements.txt b/requirements.txt index d99a7f2..60b59c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ -PySimpleGUI==4.16.0 +PySimpleGUI==4.22.0 +AnyTree==2.8.0 markdown-generator==0.1.3 +PyYAML==5.3.1 +Pillow==7.2.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 4922a71..b8d4c28 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ long_description = f.read() setup( - name='rasa-storyteller', - version='0.1', + name="rasa-storyteller", + version="0.2", author="ezhvsalate", author_email="ezhvsalate@ya.ru", description="A simple GUI utility to create complex stories for RASA chatbots easily.", @@ -15,15 +15,16 @@ packages=find_packages(), python_requires=">=3.6", install_requires=[ - 'PySimpleGUI==4.16.0', - 'markdown-generator==0.1.3', - 'PyYAML==5.3.1', - 'Pillow==7.0.0' + "PySimpleGUI==4.22.0", + "AnyTree==2.8.0", + "markdown-generator==0.1.3", + "PyYAML==5.3.1", + "Pillow==7.2.0", ], - entry_points=''' + entry_points=""" [console_scripts] rasa-storyteller=gui.run:launcher - ''', + """, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License",