Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/sync with backend #98

Merged
merged 46 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4c12044
feat: Version each of bot & df_designer dirs
Ramimashkouk Jun 7, 2024
5fc58c5
Merge branch 'dev' into feat/version-user-proj2
Ramimashkouk Aug 8, 2024
683f5f6
style: Black up
Ramimashkouk Aug 8, 2024
7ffb675
fix: Delete is_alive & slot interface
Ramimashkouk Oct 4, 2024
2c4a105
fix: Run python3 if python wasn't found
Ramimashkouk Oct 8, 2024
ca9c92f
refactor: Reorganize json_converter into classes
Ramimashkouk Oct 11, 2024
0b888b5
refactor: Add store_custom_services
Ramimashkouk Oct 11, 2024
dd4f67c
Merge branch 'fix/slots' into test/fix-tests
Ramimashkouk Oct 28, 2024
8aa4bd8
test: Add tests for new json_converter
Ramimashkouk Nov 2, 2024
43e3ff6
fix: Handle building if there's no slot_node in graph
Ramimashkouk Nov 5, 2024
c75b014
refactor: Add dev group in toml file
Ramimashkouk Nov 5, 2024
6727a18
refactor: Use ast instead of the manual old indexing
Ramimashkouk Nov 5, 2024
39f97f6
chore: Update to the new template repo
Ramimashkouk Nov 5, 2024
cc78f17
Merge branch 'feat/version-user-proj2' into refactor/roma-feedback
Ramimashkouk Nov 8, 2024
c437449
chore: Add logging to show weather it's gonna build
Ramimashkouk Nov 8, 2024
7433d33
fix: Save tg_token in .env
Ramimashkouk Nov 8, 2024
af4dab2
feat: Support automatic transitions (previous, ...)
Ramimashkouk Nov 8, 2024
42b8af1
style: Black up
Ramimashkouk Nov 8, 2024
be81934
style: Flake8 up
Ramimashkouk Nov 8, 2024
30f38c6
fix: Check for unique group names
Ramimashkouk Nov 14, 2024
463a746
feat: Integrate slots in responses by FillTemplate
Ramimashkouk Nov 14, 2024
53ea889
feat: Delete websocket and use http_interface
Ramimashkouk Nov 19, 2024
0a84d80
fix: Get last flow not last tag with `/flows`
Ramimashkouk Nov 19, 2024
6a5ef9a
chore: Write logs out in case of stopped process
Ramimashkouk Nov 20, 2024
31d1ef9
test: Fix json-converter tests
Ramimashkouk Nov 20, 2024
ca7b301
fix: Check aliveness of a tg|http process properly
Ramimashkouk Nov 27, 2024
1767b4f
fix: Version with git only when using UI
Ramimashkouk Nov 28, 2024
ba5b4a9
fix: Save built script after end of process
Ramimashkouk Nov 29, 2024
b62d9dd
fix: Return http health checking
Ramimashkouk Nov 29, 2024
11b5101
fix: Set a logger for service_replacer
Ramimashkouk Nov 29, 2024
c434d36
chore: Clean old config refreshing
Ramimashkouk Nov 29, 2024
221a2ff
fix: Save script in periodic-check& separate is_alive
Ramimashkouk Nov 29, 2024
077e758
fix: Return config reload refreshing feature
Ramimashkouk Nov 29, 2024
c1f3696
fix: Terminate a group process with its children
Ramimashkouk Dec 2, 2024
50eb381
websocket removed
artem-mar Dec 2, 2024
afb79a8
fix: Get env vars after reloading if any
Ramimashkouk Dec 2, 2024
b8e9eba
fix responses and conditions
artem-mar Dec 2, 2024
e5aedc3
token sending added
artem-mar Dec 2, 2024
7c1a12c
rename repeat to current
artem-mar Dec 2, 2024
0ac2dd4
Merge branch 'test/fix-tests-up' into fix/sync-with-backend
artem-mar Dec 2, 2024
9491d2b
chore: update dependencies
artem-mar Dec 3, 2024
dc29529
fix: fixed chat functionality
artem-mar Dec 3, 2024
3fd9580
Merge branch 'dev' into fix/sync-with-backend
Ramimashkouk Dec 16, 2024
219345a
update lock file
Ramimashkouk Dec 16, 2024
c130be1
Merge branch 'dev' into fix/sync-with-backend
Ramimashkouk Dec 16, 2024
372d79d
style: Black up
Ramimashkouk Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/chatsky_ui/services/json_converter_new2/base_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod


class BaseConverter(ABC):
def __call__(self, *args, **kwargs):
return self._convert()

@abstractmethod
def _convert(self):
raise NotImplementedError
3 changes: 3 additions & 0 deletions backend/chatsky_ui/services/json_converter_new2/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RESPONSES_FILE = "responses"
CONDITIONS_FILE = "conditions"
CUSTOM_FILE = "custom"
71 changes: 71 additions & 0 deletions backend/chatsky_ui/services/json_converter_new2/flow_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 = 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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.http is not None:
return {"chatsky.messengers.HTTPMessengerInterface": {}}
elif self.interface.telegram is not None:
return {
"chatsky.messengers.TelegramInterface": {"token": {"external:os.getenv": "TG_BOT_TOKEN"}}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from abc import ABC, abstractmethod

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 BadConditionException(Exception):
pass


class ConditionConverter(BaseConverter, ABC):
@abstractmethod
def get_pre_transitions():
raise NotImplementedError


class CustomConditionConverter(ConditionConverter):
def __init__(self, condition: dict):
self.condition = None
try:
self.condition = CustomCondition(
name=condition["name"],
code=condition["data"]["python"]["action"],
)
except KeyError as missing_key:
raise BadConditionException("Missing key in custom condition data") from missing_key

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 = None
try:
self.condition = SlotCondition(slot_id=condition["data"]["slot"], name=condition["name"])
except KeyError as missing_key:
raise BadConditionException("Missing key in slot condition data") from missing_key

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] # type: ignore
return {slot_path: {"chatsky.processing.slots.Extract": slot_path}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 BadResponseException(Exception):
pass


class ResponseConverter(BaseConverter):
pass


class TextResponseConverter(ResponseConverter):
def __init__(self, response: dict):
try:
self.response = TextResponse(
name=response["name"],
text=next(iter(response["data"]))["text"],
)
except KeyError as e:
raise BadResponseException("Missing key in custom condition data") from e

def _convert(self):
return {"chatsky.Message": {"text": self.response.text}}


class CustomResponseConverter(ResponseConverter):
def __init__(self, response: dict):
try:
self.response = CustomResponse(
name=response["name"],
code=next(iter(response["data"]))["python"]["action"],
)
except KeyError as e:
raise BadResponseException("Missing key in custom response data") from e

def _convert(self):
store_custom_service(settings.responses_path, [self.response.code])
return {f"{CUSTOM_FILE}.{RESPONSES_FILE}.{self.response.name}": None}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import ast
from ast import NodeTransformer
from typing import Dict, List
from pathlib import Path
import logging
from chatsky_ui.core.logger_config import get_logger


class ServiceReplacer(NodeTransformer):
def __init__(self, new_services: List[str]):
self.new_services_classes = self._get_classes_def(new_services)
self._logger = None

@property
def logger(self):
if self._logger is None:
raise ValueError("Logger has not been configured. Call set_logger() first.")
return self._logger

def set_logger(self):
self._logger = get_logger(__name__)

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]
for idx, parsed_code in enumerate(parsed_codes):
classes = self._extract_class_defs(parsed_code, services_code[idx])
return classes

def _extract_class_defs(self, parsed_code: ast.Module, service_code: str):
classes = {}
for node in parsed_code.body:
if isinstance(node, ast.ClassDef):
classes[node.name] = node
else:
self.logger.error("No class definition found in new_service: %s", service_code)
return classes

def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
self.logger.debug("Visiting class '%s' and comparing with: %s", node.name, self.new_services_classes.keys())
if node.name in self.new_services_classes:
return self._get_class_def(node)
return node

def _get_class_def(self, node: ast.ClassDef) -> ast.ClassDef:
service = self.new_services_classes[node.name]
del self.new_services_classes[node.name]
self.logger.info("Updating class '%s'", node.name)
return service

def generic_visit(self, node: ast.AST):
super().generic_visit(node)
if isinstance(node, ast.Module) and self.new_services_classes:
self._append_new_services(node)
return node

def _append_new_services(self, node: ast.Module):
self.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.set_logger()
replacer.visit(conditions_tree)

with open(services_path, "w") as file:
file.write(ast.unparse(conditions_tree))


def get_all_classes(services_path):
with open(services_path, "r", encoding="UTF-8") as file:
conditions_tree = ast.parse(file.read())

return [
{"name": node.name, "body": ast.unparse(node)}
for node in conditions_tree.body
if isinstance(node, ast.ClassDef)
]
105 changes: 105 additions & 0 deletions backend/chatsky_ui/services/json_converter_new2/node_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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, PRE_RESPONSE


class NodeConverter(BaseConverter):
RESPONSE_CONVERTER = {
"text": TextResponseConverter,
"python": CustomResponseConverter,
}
CONDITION_CONVERTER = {
"python": CustomConditionConverter,
"slot": SlotConditionConverter,
}

def __init__(self, config: dict):
pass


class InfoNodeConverter(NodeConverter):
MAP_TR2CHATSKY = {
"start": "dst.Start",
"fallback": "dst.Fallback",
"previous": "dst.Previous",
"repeat": "dst.Current",
}

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"]
if "dst" in condition and condition["data"]["transition_type"] == "manual"
else self.MAP_TR2CHATSKY[condition["data"]["transition_type"]],
"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()
},
PRE_RESPONSE: {"fill": {"chatsky.processing.FillTemplate": None}},
}


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
# }
Loading
Loading