From ca9c92f82792803b87dc8b9535ed12b0a06296cc Mon Sep 17 00:00:00 2001 From: Ramimashkouk Date: Fri, 11 Oct 2024 12:27:56 +0800 Subject: [PATCH] refactor: Reorganize json_converter into classes --- backend/chatsky_ui/cli.py | 8 +- .../front_graph_components/base_component.py | 4 + .../schemas/front_graph_components/flow.py | 9 ++ .../info_holders/__init__.py | 0 .../info_holders/condition.py | 13 +++ .../info_holders/response.py | 13 +++ .../front_graph_components/interface.py | 21 +++++ .../schemas/front_graph_components/node.py | 22 +++++ .../front_graph_components/pipeline.py | 8 ++ .../schemas/front_graph_components/script.py | 7 ++ .../schemas/front_graph_components/slot.py | 16 ++++ .../json_converter_new2/base_converter.py | 9 ++ .../condition_finder.py | 0 .../services/json_converter_new2/consts.py | 3 + .../json_converter_new2/flow_converter.py | 68 ++++++++++++++ .../interface_converter.py | 9 ++ .../condition_converter.py | 59 ++++++++++++ .../response_converter.py | 46 +++++++++ .../json_converter_new2/node_converter.py | 94 +++++++++++++++++++ .../json_converter_new2/pipeline_converter.py | 46 +++++++++ .../json_converter_new2/script_converter.py | 34 +++++++ .../json_converter_new2/slots_converter.py | 79 ++++++++++++++++ 22 files changed, 566 insertions(+), 2 deletions(-) create mode 100644 backend/chatsky_ui/schemas/front_graph_components/base_component.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/flow.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/info_holders/__init__.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/info_holders/response.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/interface.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/node.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/pipeline.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/script.py create mode 100644 backend/chatsky_ui/schemas/front_graph_components/slot.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/base_converter.py rename backend/chatsky_ui/services/{ => json_converter_new2}/condition_finder.py (100%) create mode 100644 backend/chatsky_ui/services/json_converter_new2/consts.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/flow_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/interface_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/node_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/script_converter.py create mode 100644 backend/chatsky_ui/services/json_converter_new2/slots_converter.py diff --git a/backend/chatsky_ui/cli.py b/backend/chatsky_ui/cli.py index a50e5376..783cd093 100644 --- a/backend/chatsky_ui/cli.py +++ b/backend/chatsky_ui/cli.py @@ -9,6 +9,7 @@ import typer from cookiecutter.main import cookiecutter from typing_extensions import Annotated +import yaml # Patch nest_asyncio before importing Chatsky nest_asyncio.apply = lambda: None @@ -93,9 +94,12 @@ def build_scenario( raise NotADirectoryError(f"Directory {project_dir} doesn't exist") settings.set_config(work_directory=project_dir) - from chatsky_ui.services.json_converter import converter # pylint: disable=C0415 + from chatsky_ui.services.json_converter_new2.pipeline_converter import PipelineConverter # pylint: disable=C0415 - asyncio.run(converter(build_id=build_id)) + pipeline_converter = PipelineConverter(pipeline_id=build_id) + pipeline_converter( + input_file=settings.frontend_flows_path, output_dir=settings.scripts_dir + ) #TODO: rename to frontend_graph_path @cli.command("run_bot") diff --git a/backend/chatsky_ui/schemas/front_graph_components/base_component.py b/backend/chatsky_ui/schemas/front_graph_components/base_component.py new file mode 100644 index 00000000..6cf3aa88 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/base_component.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class BaseComponent(BaseModel): + pass diff --git a/backend/chatsky_ui/schemas/front_graph_components/flow.py b/backend/chatsky_ui/schemas/front_graph_components/flow.py new file mode 100644 index 00000000..f838e9d9 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/flow.py @@ -0,0 +1,9 @@ +from typing import List + +from .base_component import BaseComponent + + +class Flow(BaseComponent): + name: str + nodes: List[dict] + edges: List[dict] diff --git a/backend/chatsky_ui/schemas/front_graph_components/info_holders/__init__.py b/backend/chatsky_ui/schemas/front_graph_components/info_holders/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py b/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py new file mode 100644 index 00000000..3789472f --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py @@ -0,0 +1,13 @@ +from ..base_component import BaseComponent + + +class Condition(BaseComponent): + name: str + + +class CustomCondition(Condition): + code: str + + +class SlotCondition(Condition): + slot_id: str # not the condition id diff --git a/backend/chatsky_ui/schemas/front_graph_components/info_holders/response.py b/backend/chatsky_ui/schemas/front_graph_components/info_holders/response.py new file mode 100644 index 00000000..53770fcc --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/info_holders/response.py @@ -0,0 +1,13 @@ +from ..base_component import BaseComponent + + +class Response(BaseComponent): + name: str + + +class TextResponse(Response): + text: str + + +class CustomResponse(Response): + code: str diff --git a/backend/chatsky_ui/schemas/front_graph_components/interface.py b/backend/chatsky_ui/schemas/front_graph_components/interface.py new file mode 100644 index 00000000..472fd492 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/interface.py @@ -0,0 +1,21 @@ +from pydantic import model_validator, RootModel +from typing import Any + +from .base_component import BaseComponent + + +class Interface(BaseComponent, RootModel): + @model_validator(mode="before") + def validate_interface(cls, v): + if not isinstance(v, dict): + raise ValueError('interface must be a dictionary') + if "telegram" in v: + if not isinstance(v['telegram'], dict): + raise ValueError('telegram must be a dictionary') + if 'token' not in v['telegram'] or not isinstance(v['telegram']['token'], str): + raise ValueError('telegram dictionary must contain a string token') + elif "cli" in v: + pass + else: + raise ValueError('interface must contain either telegram or cli') + return v diff --git a/backend/chatsky_ui/schemas/front_graph_components/node.py b/backend/chatsky_ui/schemas/front_graph_components/node.py new file mode 100644 index 00000000..c47e6198 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/node.py @@ -0,0 +1,22 @@ +from typing import List + +from .base_component import BaseComponent + + +class Node(BaseComponent): + id: str + + +class InfoNode(Node): + name: str + response: dict + conditions: List[dict] + + +class LinkNode(Node): + target_flow_name: str + target_node_id: str + + +class SlotsNode(Node): + groups: List[dict] diff --git a/backend/chatsky_ui/schemas/front_graph_components/pipeline.py b/backend/chatsky_ui/schemas/front_graph_components/pipeline.py new file mode 100644 index 00000000..a1c26c86 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/pipeline.py @@ -0,0 +1,8 @@ +from typing import List + +from .base_component import BaseComponent + + +class Pipeline(BaseComponent): + flows: List[dict] + interface: dict diff --git a/backend/chatsky_ui/schemas/front_graph_components/script.py b/backend/chatsky_ui/schemas/front_graph_components/script.py new file mode 100644 index 00000000..42b0cc43 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/script.py @@ -0,0 +1,7 @@ +from typing import List + +from .base_component import BaseComponent + + +class Script(BaseComponent): + flows: List[dict] diff --git a/backend/chatsky_ui/schemas/front_graph_components/slot.py b/backend/chatsky_ui/schemas/front_graph_components/slot.py new file mode 100644 index 00000000..fa1c6580 --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/slot.py @@ -0,0 +1,16 @@ +from typing import Optional, List + +from .base_component import BaseComponent + +class Slot(BaseComponent): + name: str + + +class RegexpSlot(Slot): + id: str + regexp: str + match_group_idx: Optional[int] + + +class GroupSlot(Slot): + slots: List[dict] diff --git a/backend/chatsky_ui/services/json_converter_new2/base_converter.py b/backend/chatsky_ui/services/json_converter_new2/base_converter.py new file mode 100644 index 00000000..6d654f12 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/base_converter.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +class BaseConverter(ABC): + def __call__(self, *args, **kwargs): + return self._convert() + + @abstractmethod + def _convert(self): + raise NotImplementedError diff --git a/backend/chatsky_ui/services/condition_finder.py b/backend/chatsky_ui/services/json_converter_new2/condition_finder.py similarity index 100% rename from backend/chatsky_ui/services/condition_finder.py rename to backend/chatsky_ui/services/json_converter_new2/condition_finder.py diff --git a/backend/chatsky_ui/services/json_converter_new2/consts.py b/backend/chatsky_ui/services/json_converter_new2/consts.py new file mode 100644 index 00000000..d0219028 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/consts.py @@ -0,0 +1,3 @@ +RESPONSES_FILE="responses" +CONDITIONS_FILE="conditions" +CUSTOM_FILE="custom" diff --git a/backend/chatsky_ui/services/json_converter_new2/flow_converter.py b/backend/chatsky_ui/services/json_converter_new2/flow_converter.py new file mode 100644 index 00000000..3dc3ac6e --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/flow_converter.py @@ -0,0 +1,68 @@ +from typing import Dict, List, Any, Tuple +from ...schemas.front_graph_components.flow import Flow +from .node_converter import InfoNodeConverter, LinkNodeConverter +from .base_converter import BaseConverter + + +class FlowConverter(BaseConverter): + NODE_CONVERTERS = { + "default_node": InfoNodeConverter, + "link_node": LinkNodeConverter, + } + + def __init__(self, flow: Dict[str, Any]): + self._validate_flow(flow) + self.flow = Flow( + name=flow["name"], + nodes=flow["data"]["nodes"], + edges=flow["data"]["edges"], + ) + + def __call__(self, *args, **kwargs): + self.mapped_flows = kwargs["mapped_flows"] + self.slots_conf = kwargs["slots_conf"] + self._integrate_edges_into_nodes() + return super().__call__(*args, **kwargs) + + def _validate_flow(self, flow: Dict[str, Any]): + if "data" not in flow or "nodes" not in flow["data"] or "edges" not in flow["data"]: + raise ValueError("Invalid flow structure") + + def _integrate_edges_into_nodes(self): + def _insert_dst_into_condition(node: Dict[str, Any], condition_id: str, target_node: Tuple[str, str]) -> Dict[str, Any]: + for condition in node["data"]["conditions"]: + if condition["id"] == condition_id: + condition["dst"] = target_node + return node + + maped_edges = self._map_edges() + nodes = self.flow.nodes.copy() + for edge in maped_edges: + for idx, node in enumerate(nodes): + if node["id"] == edge["source"]: + nodes[idx] = _insert_dst_into_condition(node, edge["sourceHandle"], edge["target"]) + self.flow.nodes = nodes + + def _map_edges(self) -> List[Dict[str, Any]]: + def _get_flow_and_node_names(target_node): + node_type = target_node["type"] + if node_type == "link_node": #TODO: WHY CONVERTING HERE? + return LinkNodeConverter(target_node)(mapped_flows=self.mapped_flows) + elif node_type == "default_node": + return [self.flow.name, target_node["data"]["name"]] + + edges = self.flow.edges.copy() + for edge in edges: + target_id = edge["target"] + # target_node = _find_node_by_id(target_id, self.flow.nodes) + target_node = self.mapped_flows[self.flow.name].get(target_id) + if target_node: + edge["target"] = _get_flow_and_node_names(target_node) + return edges + + def _convert(self) -> Dict[str, Any]: + converted_flow = {self.flow.name: {}} + for node in self.flow.nodes: + if node["type"] == "default_node": + converted_flow[self.flow.name].update({node["data"]["name"]: InfoNodeConverter(node)(slots_conf=self.slots_conf)}) + return converted_flow diff --git a/backend/chatsky_ui/services/json_converter_new2/interface_converter.py b/backend/chatsky_ui/services/json_converter_new2/interface_converter.py new file mode 100644 index 00000000..676ad835 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/interface_converter.py @@ -0,0 +1,9 @@ +from .base_converter import BaseConverter +from ...schemas.front_graph_components.interface import Interface + +class InterfaceConverter(BaseConverter): + def __init__(self, interface: dict): + self.interface = Interface(**interface) + + def _convert(self): + return self.interface.model_dump() diff --git a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py new file mode 100644 index 00000000..8c92d3bb --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py @@ -0,0 +1,59 @@ +from abc import ABC, abstractmethod +import ast + +from ..consts import CUSTOM_FILE, CONDITIONS_FILE +from ..base_converter import BaseConverter +from ....schemas.front_graph_components.info_holders.condition import CustomCondition, SlotCondition + + +class ConditionConverter(BaseConverter, ABC): + @abstractmethod + def get_pre_transitions(): + raise NotImplementedError + + +class CustomConditionConverter(ConditionConverter): + def __init__(self, condition: dict): + self.condition = CustomCondition( + name=condition["name"], + code=condition["data"]["python"]["action"], + ) + + def _parse_code(self): + condition_code = next(iter(ast.parse(self.condition.code).body)) + + if not isinstance(condition_code, ast.ClassDef): + raise ValueError("Condition python code is not a ClassDef") + return condition_code + + def _convert(self): + custom_cnd = { + f"{CUSTOM_FILE}.{CONDITIONS_FILE}.{self.condition.name}": None + } + return custom_cnd + + def get_pre_transitions(self): + return {} + + +class SlotConditionConverter(ConditionConverter): + def __init__(self, condition: dict): + self.condition = SlotCondition( + slot_id=condition["data"]["slot"], + name=condition["name"] + ) + + def __call__(self, *args, **kwargs): + self.slots_conf = kwargs["slots_conf"] + return super().__call__(*args, **kwargs) + + def _convert(self): + return {"chatsky.conditions.slots.SlotsExtracted": self.slots_conf[self.condition.slot_id]} + + def get_pre_transitions(self): + slot_path = self.slots_conf[self.condition.slot_id] + return { + slot_path: { + "chatsky.processing.slots.Extract": slot_path + } + } diff --git a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py new file mode 100644 index 00000000..428b30ff --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py @@ -0,0 +1,46 @@ +import ast + +from ..base_converter import BaseConverter +from ....schemas.front_graph_components.info_holders.response import TextResponse, CustomResponse +from ..consts import CUSTOM_FILE, RESPONSES_FILE + + +class ResponseConverter(BaseConverter): + pass + + +class TextResponseConverter(ResponseConverter): + def __init__(self, response: dict): + self.response = TextResponse( + name=response["name"], + text=next(iter(response["data"]))["text"], + ) + + def _convert(self): + return { + "chatsky.Message": { + "text": self.response.text + } + } + + +class CustomResponseConverter(ResponseConverter): + def __init__(self, response: dict): + # self.code = + self.response = CustomResponse( + name=response["name"], + code=next(iter(response["data"]))["python"]["action"], + ) + + def _parse_code(self): + response_code = next(iter(ast.parse(self.response.code).body)) + + if not isinstance(response_code, ast.ClassDef): + raise ValueError("Response python code is not a ClassDef") + return response_code + + def _convert(self): + return { + f"{CUSTOM_FILE}.{RESPONSES_FILE}.{self.response.name}": None + } + diff --git a/backend/chatsky_ui/services/json_converter_new2/node_converter.py b/backend/chatsky_ui/services/json_converter_new2/node_converter.py new file mode 100644 index 00000000..27e4e738 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/node_converter.py @@ -0,0 +1,94 @@ +from typing import List + +from .base_converter import BaseConverter +from ...schemas.front_graph_components.node import InfoNode, LinkNode +from .logic_component_converter.response_converter import TextResponseConverter, CustomResponseConverter +from .logic_component_converter.condition_converter import CustomConditionConverter, SlotConditionConverter + +from chatsky import RESPONSE, TRANSITIONS, PRE_TRANSITION + + +class NodeConverter(BaseConverter): + RESPONSE_CONVERTER = { + "text": TextResponseConverter, + "python": CustomResponseConverter, + } + CONDITION_CONVERTER = { + "python": CustomConditionConverter, + "slot": SlotConditionConverter, + } + + def __init__(self, config: dict): + pass + + +class InfoNodeConverter(NodeConverter): + def __init__(self, node: dict): + self.node = InfoNode( + id=node["id"], + name=node["data"]["name"], + response=node["data"]["response"], + conditions=node["data"]["conditions"], + ) + + def __call__(self, *args, **kwargs): + self.slots_conf = kwargs["slots_conf"] + return super().__call__(*args, **kwargs) + + def _convert(self): + condition_converters = [self.CONDITION_CONVERTER[condition["type"]](condition) for condition in self.node.conditions] + return { + RESPONSE: self.RESPONSE_CONVERTER[self.node.response["type"]](self.node.response)(), + TRANSITIONS: [ + { + "dst": condition["dst"], + "priority": condition["data"]["priority"], + "cnd": converter(slots_conf=self.slots_conf) + } for condition, converter in zip(self.node.conditions, condition_converters) + ], + PRE_TRANSITION: { + key: value + for converter in condition_converters + for key, value in converter.get_pre_transitions().items() + } + } + + +class LinkNodeConverter(NodeConverter): + def __init__(self, config: dict): + self.node = LinkNode( + id=config["id"], + target_flow_name=config["data"]["transition"]["target_flow"], + target_node_id=config["data"]["transition"]["target_node"], + ) + + def __call__(self, *args, **kwargs): + self.mapped_flows = kwargs["mapped_flows"] + return super().__call__(*args, **kwargs) + + def _convert(self): + return [ + self.node.target_flow_name, + self.mapped_flows[self.node.target_flow_name][self.node.target_node_id]["data"]["name"], + ] + + +class ConfNodeConverter(NodeConverter): + def __init__(self, config: dict): + super().__init__(config) + + + def _convert(self): + return { + # node.name: node._convert() for node in self.nodes + } + + +class SlotsNodeConverter(ConfNodeConverter): + def __init__(self, config: List[dict]): + self.slots = config + + def _convert(self): + return { + # node.name: node._convert() for node in self.nodes + } diff --git a/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py b/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py new file mode 100644 index 00000000..c0eeb988 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py @@ -0,0 +1,46 @@ +from pathlib import Path +import yaml +try: + from yaml import CLoader as Loader, CDumper as Dumper +except ImportError: + from yaml import Loader, Dumper + +from ...schemas.front_graph_components.pipeline import Pipeline +from ...schemas.front_graph_components.interface import Interface +from ...schemas.front_graph_components.flow import Flow + +from .base_converter import BaseConverter +from .flow_converter import FlowConverter +from .script_converter import ScriptConverter +from .interface_converter import InterfaceConverter +from .slots_converter import SlotsConverter + + +class PipelineConverter(BaseConverter): + def __init__(self, pipeline_id: int): + self.pipeline_id = pipeline_id + + def __call__(self, input_file: Path, output_dir: Path): + self.from_yaml(file_path=input_file) + self.pipeline = Pipeline(**self.graph) + self.converted_pipeline = super().__call__() + self.to_yaml(dir_path=output_dir) + + def from_yaml(self, file_path: Path): + with open(str(file_path), "r", encoding="UTF-8") as file: + self.graph = yaml.load(file, Loader=Loader) + + def to_yaml(self, dir_path: Path): + with open(f"{dir_path}/build_{self.pipeline_id}.yaml", "w", encoding="UTF-8") as file: + yaml.dump(self.converted_pipeline, file, Dumper=Dumper, default_flow_style=False) + + def _convert(self): + slots_converter = SlotsConverter(self.pipeline.flows) + slots_conf = slots_converter.map_slots() + return { + "script": ScriptConverter(self.pipeline.flows)(slots_conf=slots_conf), + "interface": InterfaceConverter(self.pipeline.interface)(), + "slots": slots_converter(), + # "start_label": self.script.get_start_label(), + # "fallback_label": self.script.get_fallback_label(), + } diff --git a/backend/chatsky_ui/services/json_converter_new2/script_converter.py b/backend/chatsky_ui/services/json_converter_new2/script_converter.py new file mode 100644 index 00000000..6d4a8991 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/script_converter.py @@ -0,0 +1,34 @@ +from typing import List + +from .base_converter import BaseConverter +from .flow_converter import FlowConverter +from ...schemas.front_graph_components.script import Script + + +class ScriptConverter(BaseConverter): + def __init__(self, flows: List[dict]): + self.script = Script(flows=flows) + self.mapped_flows = self._map_flows() #TODO: think about storing this in a temp file + + def __call__(self, *args, **kwargs): + self.slots_conf = kwargs["slots_conf"] + return super().__call__(*args, **kwargs) + + def _convert(self): + return { + key: value + for flow in self.script.flows + for key, value in FlowConverter(flow)( + mapped_flows=self.mapped_flows, + slots_conf=self.slots_conf + ).items() + } + + def _map_flows(self): + mapped_flows = {} + for flow in self.script.flows: + mapped_flows[flow["name"]] = {} + for node in flow["data"]["nodes"]: + mapped_flows[flow["name"]][node["id"]] = node + return mapped_flows + diff --git a/backend/chatsky_ui/services/json_converter_new2/slots_converter.py b/backend/chatsky_ui/services/json_converter_new2/slots_converter.py new file mode 100644 index 00000000..7694d8d0 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/slots_converter.py @@ -0,0 +1,79 @@ +from typing import List + +from .base_converter import BaseConverter +from ...schemas.front_graph_components.slot import GroupSlot, RegexpSlot +from ...schemas.front_graph_components.node import SlotsNode + +class SlotsConverter(BaseConverter): + def __init__(self, flows: List[dict]): + def _get_slots_node(flows): + return next(iter([ + node + for flow in flows + for node in flow["data"]["nodes"] + if node["type"] == "slots_node" + ])) + + slots_node = _get_slots_node(flows) + self.slots_node = SlotsNode( + id=slots_node["id"], + groups=slots_node["data"]["groups"], + ) + + def map_slots(self): + mapped_slots = {} + for group in self.slots_node.groups.copy(): + for slot in group["slots"]: + mapped_slots[slot["id"]] = ".".join([group["name"], slot["name"]]) + return mapped_slots + + def _convert(self): + return { + key: value + for group in self.slots_node.groups + for key, value in GroupSlotConverter(group)().items() + } + +class RegexpSlotConverter(SlotsConverter): + def __init__(self, slot: dict): + self.slot = RegexpSlot( + id=slot["id"], + name=slot["name"], + regexp=slot["value"], + match_group_idx=slot.get("match_group_idx", None), + ) + + def _convert(self): + return { + self.slot.name: { + "chatksy.slots.RegexpSlot": { + "regexp": self.slot.regexp, + "match_group_idx": self.slot.match_group_idx, + } + } + } + + +class GroupSlotConverter(SlotsConverter): + SLOTS_CONVERTER_TYPES = { + "GroupSlot": "self", # Placeholder, will be replaced in __init__ + "RegexpSlot": RegexpSlotConverter, + } + + def __init__(self, slot: dict): + # Replace the placeholder with the actual class reference + self.SLOTS_CONVERTER_TYPES["GroupSlot"] = GroupSlotConverter + + self.slot = GroupSlot( + name=slot["name"], + slots=slot["slots"], + ) + + def _convert(self): + return { + self.slot.name: { + key: value + for slot in self.slot.slots + for key, value in self.SLOTS_CONVERTER_TYPES[slot["type"]](slot)().items() + } + }