From d0ecd10b2281a8abe59abdcedf864b7f720a9569 Mon Sep 17 00:00:00 2001 From: Rami <54779216+Ramimashkouk@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:46:09 +0300 Subject: [PATCH] refactor json converter2 (#95) * refactor: Reorganize json_converter into classes * refactor: Add store_custom_services --- 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 | 22 +++++ .../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 ++ .../services/json_converter_new2/consts.py | 3 + .../json_converter_new2/flow_converter.py | 68 ++++++++++++++ .../interface_converter.py | 14 +++ .../condition_converter.py | 55 +++++++++++ .../response_converter.py | 40 ++++++++ .../service_replacer.py} | 23 ++++- .../json_converter_new2/node_converter.py | 94 +++++++++++++++++++ .../json_converter_new2/pipeline_converter.py | 52 ++++++++++ .../json_converter_new2/script_converter.py | 54 +++++++++++ .../json_converter_new2/slots_converter.py | 79 ++++++++++++++++ 22 files changed, 606 insertions(+), 7 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 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 rename backend/chatsky_ui/services/{condition_finder.py => json_converter_new2/logic_component_converter/service_replacer.py} (72%) 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..c5bafbfb --- /dev/null +++ b/backend/chatsky_ui/schemas/front_graph_components/interface.py @@ -0,0 +1,22 @@ +from pydantic import Field, model_validator +from typing import Any + +from .base_component import BaseComponent +from typing import Optional, Dict + +class Interface(BaseComponent): + telegram: Optional[Dict[str, Any]] = Field(default=None) + cli: Optional[Dict[str, Any]] = Field(default=None) + + @model_validator(mode='after') + def check_one_not_none(cls, values): + telegram, cli = values.telegram, values.cli + if (telegram is None) == (cli is None): + raise ValueError('Exactly one of "telegram" or "cli" must be provided.') + return values + + @model_validator(mode='after') + def check_telegram_token(cls, values): + if values.telegram is not None and 'token' not in values.telegram: + raise ValueError('Telegram token must be provided.') + return values 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..1ef338d1 --- /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: 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/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..42ba348a --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/interface_converter.py @@ -0,0 +1,14 @@ +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): + if self.interface.cli is not None: + return {"chatsky.messengers.console.CLIMessengerInterface": {}} + elif self.interface.telegram is not None: + return { + "chatsky.messengers.telegram.LongpollingInterface": {"token": self.interface.telegram["token"]} + } 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..0c25737f --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py @@ -0,0 +1,55 @@ +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 +from ....core.config import settings +from .service_replacer import store_custom_service + + +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 _convert(self): + store_custom_service(settings.conditions_path, [self.condition.code]) + 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..23e92639 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py @@ -0,0 +1,40 @@ +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 +from ....core.config import settings +from .service_replacer import store_custom_service + + +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.response = CustomResponse( + name=response["name"], + code=next(iter(response["data"]))["python"]["action"], + ) + + def _convert(self): + store_custom_service(settings.responses_path, [self.response.code]) + return { + f"{CUSTOM_FILE}.{RESPONSES_FILE}.{self.response.name}": None + } diff --git a/backend/chatsky_ui/services/condition_finder.py b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py similarity index 72% rename from backend/chatsky_ui/services/condition_finder.py rename to backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py index 09d94203..4f601fc9 100644 --- a/backend/chatsky_ui/services/condition_finder.py +++ b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py @@ -1,6 +1,7 @@ import ast from ast import NodeTransformer from typing import Dict, List +from pathlib import Path from chatsky_ui.core.logger_config import get_logger @@ -13,17 +14,18 @@ def __init__(self, new_services: List[str]): def _get_classes_def(self, services_code: List[str]) -> Dict[str, ast.ClassDef]: parsed_codes = [ast.parse(service_code) for service_code in services_code] - result_nodes = {} for idx, parsed_code in enumerate(parsed_codes): - self._extract_class_defs(parsed_code, result_nodes, services_code[idx]) - return result_nodes + classes = self._extract_class_defs(parsed_code, services_code[idx]) + return classes - def _extract_class_defs(self, parsed_code: ast.Module, result_nodes: Dict[str, ast.ClassDef], service_code: str): + def _extract_class_defs(self, parsed_code: ast.Module, service_code: str): + classes = {} for node in parsed_code.body: if isinstance(node, ast.ClassDef): - result_nodes[node.name] = node + classes[node.name] = node else: logger.error("No class definition found in new_service: %s", service_code) + return classes def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: logger.debug("Visiting class '%s' and comparing with: %s", node.name, self.new_services_classes.keys()) @@ -46,3 +48,14 @@ def _append_new_services(self, node: ast.Module): logger.info("Services not found, appending new services: %s", list(self.new_services_classes.keys())) for _, service in self.new_services_classes.items(): node.body.append(service) + + +def store_custom_service(services_path: Path, services: List[str]): + with open(services_path, "r", encoding="UTF-8") as file: + conditions_tree = ast.parse(file.read()) + + replacer = ServiceReplacer(services) + replacer.visit(conditions_tree) + + with open(services_path, "w") as file: + file.write(ast.unparse(conditions_tree)) 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..210f121f --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py @@ -0,0 +1,52 @@ +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) + script_converter = ScriptConverter(self.pipeline.flows) + + slots_conf = slots_converter.map_slots() + start_label, fallback_label = script_converter.extract_start_fallback_labels() + + return { + "script": script_converter(slots_conf=slots_conf), + "messenger_interface": InterfaceConverter(self.pipeline.interface)(), + "slots": slots_converter(), + "start_label": start_label, + "fallback_label": 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..dab6df66 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter_new2/script_converter.py @@ -0,0 +1,54 @@ +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 + + def extract_start_fallback_labels(self): #TODO: refactor this huge method + start_label, fallback_label = None, None + + for flow in self.script.flows: + for node in flow["data"]["nodes"]: + flags = node["data"]["flags"] + + if "start" in flags: + if start_label: + raise ValueError("Multiple start nodes found") + start_label = [flow["name"], node["data"]["name"]] + if "fallback" in flags: + if fallback_label: + raise ValueError("Multiple fallback nodes found") + fallback_label = [flow["name"], node["data"]["name"]] + + if start_label and fallback_label: + return start_label, fallback_label + + return None, None 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..1966b8d0 --- /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", 1), + ) + + def _convert(self): + return { + self.slot.name: { + "chatsky.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() + } + }