From dc01db60c7f3639384d669ed0454bdfb9b3007e1 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 10 Jul 2024 07:32:14 +0500 Subject: [PATCH 01/86] most things done except components -> before + after handler, clarification required --- chatsky/pipeline/pipeline/actor.py | 42 ++++++--- chatsky/pipeline/pipeline/component.py | 93 +++++++++---------- chatsky/pipeline/pipeline/pipeline.py | 101 +++++++++++---------- chatsky/pipeline/service/extra.py | 39 ++------ chatsky/pipeline/service/group.py | 114 +++++++---------------- chatsky/pipeline/service/service.py | 121 ++++++------------------- chatsky/pipeline/types.py | 85 ----------------- 7 files changed, 191 insertions(+), 404 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 6f0256885..7fed26642 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -67,18 +67,20 @@ class Actor: - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. """ - def __init__( - self, - script: Union[Script, dict], - start_label: NodeLabel2Type, - fallback_label: Optional[NodeLabel2Type] = None, - label_priority: float = 1.0, - condition_handler: Optional[Callable] = None, - handlers: Optional[Dict[ActorStage, List[Callable]]] = None, - ): + # Can this just be Script, since Actor is now pydantic BaseModel? + # I feel like this is a bit different and is already handled. + script: Union[Script, dict] + start_label: NodeLabel2Type + fallback_label: Optional[NodeLabel2Type] = None + label_priority: float = 1.0 + condition_handler: Optional[Callable] = Field(default=default_condition_handler) + handlers: Optional[Dict[ActorStage, List[Callable]]] = {} + _clean_turn_cache: Optional[bool] = True + # Making a 'computed field' for this feels overkill, a 'private' field is probably fine? + + @model_validator(mode="after") + def actor_validator(self): self.script = script if isinstance(script, Script) else Script(script=script) - self.label_priority = label_priority - self.start_label = normalize_label(start_label) if self.script.get(self.start_label[0], {}).get(self.start_label[1]) is None: raise ValueError(f"Unknown start_label={self.start_label}") @@ -89,13 +91,25 @@ def __init__( self.fallback_label = normalize_label(fallback_label) if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: raise ValueError(f"Unknown fallback_label={self.fallback_label}") - self.condition_handler = default_condition_handler if condition_handler is None else condition_handler - - self.handlers = {} if handlers is None else handlers # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: + """ + Method for running an `Actor`. + Catches runtime exceptions and logs them. + + :param ctx: Current dialog context. + :param pipeline: Current pipeline. + """ + try: + # This line could be: "await self(pipeline, ctx)", but I'm not sure. + await pipeline.actor(pipeline, ctx) + except Exception as exc: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"Actor '{self.name}' execution failed!", exc_info=exc) + async def __call__(self, pipeline: Pipeline, ctx: Context): await self._run_handlers(ctx, pipeline, ActorStage.CONTEXT_INIT) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index ab37426ea..14a5fc891 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -14,10 +14,11 @@ import abc import asyncio from typing import Optional, Awaitable, TYPE_CHECKING +from pydantic import BaseModel, Field, model_validator from chatsky.script import Context -from ..service.extra import BeforeHandler, AfterHandler +from ..service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from ..conditions import always_start_condition from ..types import ( StartConditionCheckerFunction, @@ -26,7 +27,6 @@ GlobalExtraHandlerType, ExtraHandlerFunction, ExtraHandlerType, - ExtraHandlerBuilder, ) logger = logging.getLogger(__name__) @@ -35,15 +35,16 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class PipelineComponent(abc.ABC): +# arbitrary_types_allowed for testing, will remove later +class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allowed=True): """ This class represents a pipeline component, which is a service or a service group. It contains some fields that they have in common. :param before_handler: :py:class:`~.BeforeHandler`, associated with this component. - :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :type before_handler: Optional[:py:data:`~.ComponentExtraHandler`] :param after_handler: :py:class:`~.AfterHandler`, associated with this component. - :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :type after_handler: Optional[:py:data:`~.ComponentExtraHandler`] :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), if it exceeds this time, it is interrupted. :param requested_async_flag: Requested asynchronous property; @@ -60,50 +61,28 @@ class PipelineComponent(abc.ABC): :param path: Separated by dots path to component, is universally unique. """ - def __init__( - self, - before_handler: Optional[ExtraHandlerBuilder] = None, - after_handler: Optional[ExtraHandlerBuilder] = None, - timeout: Optional[float] = None, - requested_async_flag: Optional[bool] = None, - calculated_async_flag: bool = False, - start_condition: Optional[StartConditionCheckerFunction] = None, - name: Optional[str] = None, - path: Optional[str] = None, - ): - self.timeout = timeout - """ - Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted (for asynchronous only!). - """ - self.requested_async_flag = requested_async_flag - """Requested asynchronous property; if not defined, :py:attr:`~requested_async_flag` is used instead.""" - self.calculated_async_flag = calculated_async_flag - """Calculated asynchronous property, whether the component can be asynchronous or not.""" - self.start_condition = always_start_condition if start_condition is None else start_condition - """ - Component start condition that is invoked before each component execution; - component is executed only if it returns `True`. - """ - self.name = name - """ - Component name (should be unique in single :py:class:`~pipeline.service.group.ServiceGroup`), - should not be blank or contain '.' symbol. - """ - self.path = path - """ - Dot-separated path to component (is universally unique). - This attribute is set in :py:func:`~chatsky.pipeline.pipeline.utils.finalize_service_group`. - """ + before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) + after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) + timeout: Optional[float] = None + requested_async_flag: Optional[bool] = None + calculated_async_flag: bool = False + # Is the Field here correct? I'll check later. + start_condition: Optional[StartConditionCheckerFunction] = Field(default=always_start_condition) + name: Optional[str] = None + path: Optional[str] = None + + @model_validator(mode="after") + def pipeline_component_validator(self): + self.start_condition = always_start_condition if self.start_condition is None else self.start_condition - self.before_handler = BeforeHandler([] if before_handler is None else before_handler) - self.after_handler = AfterHandler([] if after_handler is None else after_handler) + if self.name is not None and (self.name == "" or "." in self.name): + raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {self.name})!") - if name is not None and (name == "" or "." in name): - raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {name})!") + self.calculated_async_flag = all([service.asynchronous for service in self.components]) - if not calculated_async_flag and requested_async_flag: - raise Exception(f"{type(self).__name__} '{name}' can't be asynchronous!") + if not self.calculated_async_flag and self.requested_async_flag: + raise Exception(f"{type(self).__name__} '{self.name}' can't be asynchronous!") + return self def _set_state(self, ctx: Context, value: ComponentExecutionState): """ @@ -159,16 +138,34 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin except asyncio.TimeoutError: logger.warning(f"{type(self).__name__} '{self.name}' {extra_handler.stage} extra handler timed out!") + # Named this run_component, because ServiceGroup and Actor are components now too, + # and naming this run_service wouldn't be on point, they're not just services. + # The only problem I have is that this is kind of too generic, even confusingly generic, since _run() exists. + # Possible solution: implement _run within these classes themselves. + # My problem: centralizing Extra Handlers within PipelineComponent feels right. Why should Services have the right to run Extra Handlers however they want? They were already run there without checking the start_condition, which was a mistake. + @abstractmethod + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: + raise NotImplementedError + @abc.abstractmethod async def _run(self, ctx: Context, pipeline: Pipeline) -> None: """ - A method for running pipeline component, it is overridden in all its children. + A method for running a pipeline component. Executes extra handlers before and after execution, launches `run_component` method. This method is run after the component's timeout is set (if needed). :param ctx: Current dialog :py:class:`~.Context`. :param pipeline: This :py:class:`~.Pipeline`. """ - raise NotImplementedError + if await self.start_condition(ctx, pipeline): + await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) + + self._set_state(ctx, ComponentExecutionState.RUNNING) + await self.run_component(ctx, pipeline) + self._set_state(ctx, ComponentExecutionState.FINISHED) + + await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) + else: + self._set_state(ctx, ComponentExecutionState.NOT_RUN) async def __call__(self, ctx: Context, pipeline: Pipeline) -> Optional[Awaitable]: """ diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 92d62f684..5e90a1d53 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -17,6 +17,7 @@ import asyncio import logging from typing import Union, List, Dict, Optional, Hashable, Callable +from pydantic import BaseModel, Field, model_validator from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -27,37 +28,41 @@ from chatsky.messengers.common import MessengerInterface from chatsky.slots.slots import GroupSlot from ..service.group import ServiceGroup +from ..service.extra import _ComponentExtraHandler from ..types import ( - ServiceBuilder, - ServiceGroupBuilder, - PipelineBuilder, + ServiceFunction, GlobalExtraHandlerType, ExtraHandlerFunction, - ExtraHandlerBuilder, ) from .utils import finalize_service_group, pretty_format_component_info_dict from chatsky.pipeline.pipeline.actor import Actor +# """ +# Debug code. No need to look here. +# from ..service.group import Service +# from chatsky.pipeline.service.extra import _ComponentExtraHandler +# """ + logger = logging.getLogger(__name__) ACTOR = "ACTOR" -class Pipeline: +# Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. +class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: - :param components: (required) A :py:data:`~.ServiceGroupBuilder` object, + :param components: (required) A :py:data:`~.ServiceGroup` object, that will be transformed to root service group. It should include :py:class:`~.Actor`, but only once (raises exception otherwise). It will always be named pipeline. :param script: (required) A :py:class:`~.Script` instance (object or dict). :param start_label: (required) Actor start label. :param fallback_label: Actor fallback label. - :param label_priority: Default priority value for all actor :py:const:`labels ` + :param label_priority: Default priority value for all actor :py:const:`labels ` where there is no priority. Defaults to `1.0`. :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. - :param slots: Slots configuration. :param handlers: This variable is responsible for the usage of external handlers on the certain stages of work of :py:class:`~chatsky.script.Actor`. @@ -67,13 +72,13 @@ class Pipeline: :param messenger_interface: An `AbsMessagingInterface` instance for this pipeline. :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline or a dict to store dialog :py:class:`~.Context`. - :param before_handler: List of `ExtraHandlerBuilder` to add to the group. - :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] - :param after_handler: List of `ExtraHandlerBuilder` to add to the group. - :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param before_handler: List of `_ComponentExtraHandler` to add to the group. + :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] + :param after_handler: List of `_ComponentExtraHandler` to add to the group. + :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] :param timeout: Timeout to add to pipeline root service group. :param optimization_warnings: Asynchronous pipeline optimization check request flag; - warnings will be sent to logs. Additionally it has some calculated fields: + warnings will be sent to logs. Additionally, it has some calculated fields: - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, - `actor` is a pipeline actor, found among services. @@ -83,35 +88,34 @@ class Pipeline: """ - def __init__( - self, - components: ServiceGroupBuilder, - script: Union[Script, Dict], - start_label: NodeLabel2Type, - fallback_label: Optional[NodeLabel2Type] = None, - label_priority: float = 1.0, - condition_handler: Optional[Callable] = None, - slots: Optional[Union[GroupSlot, Dict]] = None, - handlers: Optional[Dict[ActorStage, List[Callable]]] = None, - messenger_interface: Optional[MessengerInterface] = None, - context_storage: Optional[Union[DBContextStorage, Dict]] = None, - before_handler: Optional[ExtraHandlerBuilder] = None, - after_handler: Optional[ExtraHandlerBuilder] = None, - timeout: Optional[float] = None, - optimization_warnings: bool = False, - parallelize_processing: bool = False, - ): - self.actor: Actor = None - self.messenger_interface = CLIMessengerInterface() if messenger_interface is None else messenger_interface - self.context_storage = {} if context_storage is None else context_storage - self.slots = GroupSlot.model_validate(slots) if slots is not None else None + components: Union[ServiceGroup, dict, List] + script: Union[Script, Dict] + start_label: NodeLabel2Type + fallback_label: Optional[NodeLabel2Type] = None + label_priority: float = 1.0 + condition_handler: Optional[Callable] = None + handlers: Optional[Dict[ActorStage, List[Callable]]] = None + messenger_interface: Optional[MessengerInterface] = Field(default_factory=CLIMessengerInterface) + context_storage: Optional[Union[DBContextStorage, Dict]] = None + before_handler: Optional[_ComponentExtraHandler] = None + after_handler: Optional[_ComponentExtraHandler] = None + timeout: Optional[float] = None + optimization_warnings: bool = False + parallelize_processing: bool = False + # TO-DO: Remove/change three parameters below (if possible) + actor: Optional[Actor] = None + _services_pipeline: Optional[ServiceGroup] + _clean_turn_cache: Optional[bool] + + @model_validator(mode="after") + def pipeline_init(self): + self.actor = None self._services_pipeline = ServiceGroup( - components, - before_handler=before_handler, - after_handler=after_handler, - timeout=timeout, + components=self.components, + before_handler=self.before_handler, + after_handler=self.after_handler, + timeout=self.timeout, ) - self._services_pipeline.name = "pipeline" self._services_pipeline.path = ".pipeline" actor_exists = finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) @@ -119,25 +123,24 @@ def __init__( raise Exception("Actor not found in the pipeline!") else: self.set_actor( - script, - start_label, - fallback_label, - label_priority, - condition_handler, - handlers, + self.script, + self.start_label, + self.fallback_label, + self.label_priority, + self.condition_handler, + self.handlers, ) if self.actor is None: raise Exception("Actor wasn't initialized correctly!") - if optimization_warnings: + if self.optimization_warnings: self._services_pipeline.log_optimization_warnings() - self.parallelize_processing = parallelize_processing - # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True if self._clean_turn_cache: self.actor._clean_turn_cache = False + return self def add_global_handler( self, diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 8a8a65a9b..aedec2a16 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -19,7 +19,6 @@ from ..types import ( ServiceRuntimeInfo, ExtraHandlerType, - ExtraHandlerBuilder, ExtraHandlerFunction, ExtraHandlerRuntimeInfo, ) @@ -47,36 +46,14 @@ class _ComponentExtraHandler: :param asynchronous: Requested asynchronous property. """ - def __init__( - self, - functions: ExtraHandlerBuilder, - stage: ExtraHandlerType = ExtraHandlerType.UNDEFINED, - timeout: Optional[float] = None, - asynchronous: Optional[bool] = None, - ): - overridden_parameters = collect_defined_constructor_parameters_to_dict( - timeout=timeout, asynchronous=asynchronous - ) - if isinstance(functions, _ComponentExtraHandler): - self.__init__( - **_get_attrs_with_updates( - functions, - ("calculated_async_flag", "stage"), - {"requested_async_flag": "asynchronous"}, - overridden_parameters, - ) - ) - elif isinstance(functions, dict): - functions.update(overridden_parameters) - self.__init__(**functions) - elif isinstance(functions, List): - self.functions = functions - self.timeout = timeout - self.requested_async_flag = asynchronous - self.calculated_async_flag = all([asyncio.iscoroutinefunction(func) for func in self.functions]) - self.stage = stage - else: - raise Exception(f"Unknown type for {type(self).__name__} {functions}") + functions: ExtraHandlerFunction + stage: ExtraHandlerType = ExtraHandlerType.UNDEFINED + timeout: Optional[float] = None + requested_async_flag: Optional[bool] = None + + @computed_field(alias="calculated_async_flag", repr=False) + def calculate_async_flag(self) -> bool: + return all([asyncio.iscoroutinefunction(func) for func in self.functions]) @property def asynchronous(self) -> bool: diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 22b8bae0d..6f78f9e41 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,19 +12,18 @@ import asyncio import logging from typing import Optional, List, Union, Awaitable, TYPE_CHECKING +from pydantic import model_validator from chatsky.script import Context +from .extra import ComponentExtraHandler -from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates from ..pipeline.component import PipelineComponent from ..types import ( StartConditionCheckerFunction, ComponentExecutionState, - ServiceGroupBuilder, GlobalExtraHandlerType, ExtraHandlerConditionFunction, ExtraHandlerFunction, - ExtraHandlerBuilder, ExtraHandlerType, ) from .service import Service @@ -34,8 +33,8 @@ if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline - -class ServiceGroup(PipelineComponent): +# arbitrary_types_allowed for testing, will remove later +class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ A service group class. Service group can be included into pipeline as an object or a pipeline component list. @@ -44,12 +43,12 @@ class ServiceGroup(PipelineComponent): Components in asynchronous groups are executed simultaneously. Group can be asynchronous only if all components in it are asynchronous. - :param components: A `ServiceGroupBuilder` object, that will be added to the group. - :type components: :py:data:`~.ServiceGroupBuilder` - :param before_handler: List of `ExtraHandlerBuilder` to add to the group. - :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] - :param after_handler: List of `ExtraHandlerBuilder` to add to the group. - :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param components: A `ServiceGroup` object, that will be added to the group. + :type components: :py:data:`~.ServiceGroup` + :param before_handler: List of `_ComponentExtraHandler` to add to the group. + :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] + :param after_handler: List of `_ComponentExtraHandler` to add to the group. + :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] :param timeout: Timeout to add to the group. :param asynchronous: Requested asynchronous property. :param start_condition: :py:data:`~.StartConditionCheckerFunction` that is invoked before each group execution; @@ -57,47 +56,22 @@ class ServiceGroup(PipelineComponent): :param name: Requested group name. """ - def __init__( - self, - components: ServiceGroupBuilder, - before_handler: Optional[ExtraHandlerBuilder] = None, - after_handler: Optional[ExtraHandlerBuilder] = None, - timeout: Optional[float] = None, - asynchronous: Optional[bool] = None, - start_condition: Optional[StartConditionCheckerFunction] = None, - name: Optional[str] = None, - ): - overridden_parameters = collect_defined_constructor_parameters_to_dict( - before_handler=before_handler, - after_handler=after_handler, - timeout=timeout, - asynchronous=asynchronous, - start_condition=start_condition, - name=name, - ) - if isinstance(components, ServiceGroup): - self.__init__( - **_get_attrs_with_updates( - components, - ( - "calculated_async_flag", - "path", - ), - {"requested_async_flag": "asynchronous"}, - overridden_parameters, - ) - ) - elif isinstance(components, dict): - components.update(overridden_parameters) - self.__init__(**components) - elif isinstance(components, List): - self.components = self._create_components(components) - calc_async = all([service.asynchronous for service in self.components]) - super(ServiceGroup, self).__init__( - before_handler, after_handler, timeout, asynchronous, calc_async, start_condition, name - ) - else: - raise Exception(f"Unknown type for ServiceGroup {components}") + components: List[PipelineComponent] + + # Should these be removed from API reference? + # before_handler: Optional[ComponentExtraHandler] = None + # after_handler: Optional[ComponentExtraHandler] = None + # timeout: Optional[float] = None + # asynchronous: Optional[bool] = None + # start_condition: Optional[StartConditionCheckerFunction] = None + # name: Optional[str] = None + + # Is there a better way to do this? calculated_async_flag is exposed to the user right now. + # Of course, they might not want to break their own program, but what if. + # Maybe I could just make this a 'private' field, like '_calc_async' + @model_validator(mode="after") + def calculate_async_flag(self): + self.calculated_async_flag = all([service.asynchronous for service in self.components]) async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> None: """ @@ -110,8 +84,6 @@ async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> None: :param ctx: Current dialog context. :param pipeline: The current pipeline. """ - self._set_state(ctx, ComponentExecutionState.RUNNING) - if self.asynchronous: service_futures = [service(ctx, pipeline) for service in self.components] for service, future in zip(self.components, await asyncio.gather(*service_futures, return_exceptions=True)): @@ -127,35 +99,29 @@ async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> None: if service.asynchronous and isinstance(service_result, Awaitable): await service_result + # This gets overwritten with "FINISHED" at PipelineComponent.run(). + # TODO: resolve this conflict. failed = any([service.get_state(ctx) == ComponentExecutionState.FAILED for service in self.components]) self._set_state(ctx, ComponentExecutionState.FAILED if failed else ComponentExecutionState.FINISHED) - async def _run( + async def run_component( self, ctx: Context, pipeline: Pipeline, ) -> None: """ Method for handling this group execution. - Executes extra handlers before and after execution, checks start condition and catches runtime exceptions. + Catches runtime exceptions and logs them. :param ctx: Current dialog context. :param pipeline: The current pipeline. """ - await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) - try: - if self.start_condition(ctx, pipeline): - await self._run_services_group(ctx, pipeline) - else: - self._set_state(ctx, ComponentExecutionState.NOT_RUN) - + await self._run_services_group(ctx, pipeline) except Exception as exc: self._set_state(ctx, ComponentExecutionState.FAILED) logger.error(f"ServiceGroup '{self.name}' execution failed!", exc_info=exc) - await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) - def log_optimization_warnings(self): """ Method for logging service group optimization warnings for all this groups inner components. @@ -228,21 +194,3 @@ def info_dict(self) -> dict: representation.update({"services": [service.info_dict for service in self.components]}) return representation - @staticmethod - def _create_components(services: ServiceGroupBuilder) -> List[Union[Service, "ServiceGroup"]]: - """ - Utility method, used to create inner components, judging by their nature. - Services are created from services and dictionaries. - ServiceGroups are created from service groups and lists. - - :param services: ServiceGroupBuilder object (a `ServiceGroup` instance or a list). - :type services: :py:data:`~.ServiceGroupBuilder` - :return: List of services and service groups. - """ - handled_services: List[Union[Service, "ServiceGroup"]] = [] - for service in services: - if isinstance(service, List) or isinstance(service, ServiceGroup): - handled_services.append(ServiceGroup(service)) - else: - handled_services.append(Service(service)) - return handled_services diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index fdf43f0bb..647a9a7c3 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,17 +13,18 @@ from __future__ import annotations import logging import inspect -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Union, List from chatsky.script import Context + +from .extra import ComponentExtraHandler from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from ..types import ( ServiceBuilder, StartConditionCheckerFunction, ComponentExecutionState, - ExtraHandlerBuilder, ExtraHandlerType, ) from ..pipeline.component import PipelineComponent @@ -34,7 +35,8 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class Service(PipelineComponent): +# arbitrary_types_allowed for testing, will remove later +class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ This class represents a service. Service can be included into pipeline as object or a dictionary. @@ -42,11 +44,11 @@ class Service(PipelineComponent): Service can be asynchronous only if its handler is a coroutine. :param handler: A service function or an actor. - :type handler: :py:data:`~.ServiceBuilder` - :param before_handler: List of `ExtraHandlerBuilder` to add to the group. - :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] - :param after_handler: List of `ExtraHandlerBuilder` to add to the group. - :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :type handler: :py:data:`~.ServiceFunction` + :param before_handler: List of `_ComponentExtraHandler` to add to the group. + :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] + :param after_handler: List of `_ComponentExtraHandler` to add to the group. + :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] :param timeout: Timeout to add to the group. :param asynchronous: Requested asynchronous property. :param start_condition: StartConditionCheckerFunction that is invoked before each service execution; @@ -55,52 +57,19 @@ class Service(PipelineComponent): :param name: Requested service name. """ - def __init__( - self, - handler: ServiceBuilder, - before_handler: Optional[ExtraHandlerBuilder] = None, - after_handler: Optional[ExtraHandlerBuilder] = None, - timeout: Optional[float] = None, - asynchronous: Optional[bool] = None, - start_condition: Optional[StartConditionCheckerFunction] = None, - name: Optional[str] = None, - ): - overridden_parameters = collect_defined_constructor_parameters_to_dict( - before_handler=before_handler, - after_handler=after_handler, - timeout=timeout, - asynchronous=asynchronous, - start_condition=start_condition, - name=name, - ) - if isinstance(handler, dict): - handler.update(overridden_parameters) - self.__init__(**handler) - elif isinstance(handler, Service): - self.__init__( - **_get_attrs_with_updates( - handler, - ( - "calculated_async_flag", - "path", - ), - {"requested_async_flag": "asynchronous"}, - overridden_parameters, - ) - ) - elif callable(handler) or isinstance(handler, str) and handler == "ACTOR": - self.handler = handler - super(Service, self).__init__( - before_handler, - after_handler, - timeout, - True, - True, - start_condition, - name, - ) - else: - raise Exception(f"Unknown type of service handler: {handler}") + handler: ServiceFunction + # Should these be removed from the above API reference? I think they're still useful for users if included in API reference. + # before_handler: Optional[ComponentExtraHandler] = None + # after_handler: Optional[ComponentExtraHandler] = None + # timeout: Optional[float] = None + # asynchronous: Optional[bool] = None + calculated_async_flag: Optional[bool] = True + # start_condition: Optional[StartConditionCheckerFunction] = None + # name: Optional[str] = None + + @model_validator(mode="after") + def tick_async_flag(self): + self.calculated_async_flag = True async def _run_handler(self, ctx: Context, pipeline: Pipeline) -> None: """ @@ -127,56 +96,20 @@ async def _run_handler(self, ctx: Context, pipeline: Pipeline) -> None: else: raise Exception(f"Too many parameters required for service '{self.name}' handler: {handler_params}!") - async def _run_as_actor(self, ctx: Context, pipeline: Pipeline) -> None: - """ - Method for running this service if its handler is an `Actor`. - Catches runtime exceptions. - - :param ctx: Current dialog context. - """ - try: - await pipeline.actor(pipeline, ctx) - self._set_state(ctx, ComponentExecutionState.FINISHED) - except Exception as exc: - self._set_state(ctx, ComponentExecutionState.FAILED) - logger.error(f"Actor '{self.name}' execution failed!", exc_info=exc) - - async def _run_as_service(self, ctx: Context, pipeline: Pipeline) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ - Method for running this service if its handler is not an Actor. - Checks start condition and catches runtime exceptions. + Method for running this service. + Catches runtime exceptions and logs them. :param ctx: Current dialog context. :param pipeline: Current pipeline. """ try: - if self.start_condition(ctx, pipeline): - self._set_state(ctx, ComponentExecutionState.RUNNING) - await self._run_handler(ctx, pipeline) - self._set_state(ctx, ComponentExecutionState.FINISHED) - else: - self._set_state(ctx, ComponentExecutionState.NOT_RUN) + await self._run_handler(ctx, pipeline) except Exception as exc: self._set_state(ctx, ComponentExecutionState.FAILED) logger.error(f"Service '{self.name}' execution failed!", exc_info=exc) - async def _run(self, ctx: Context, pipeline: Pipeline) -> None: - """ - Method for handling this service execution. - Executes extra handlers before and after execution, launches `_run_as_actor` or `_run_as_service` method. - - :param ctx: (required) Current dialog context. - :param pipeline: the current pipeline. - """ - await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) - - if isinstance(self.handler, str) and self.handler == "ACTOR": - await self._run_as_actor(ctx, pipeline) - else: - await self._run_as_service(ctx, pipeline) - - await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) - @property def info_dict(self) -> dict: """ diff --git a/chatsky/pipeline/types.py b/chatsky/pipeline/types.py index 118532559..943b7f1f1 100644 --- a/chatsky/pipeline/types.py +++ b/chatsky/pipeline/types.py @@ -179,88 +179,3 @@ class ExtraHandlerRuntimeInfo(BaseModel): Can be both synchronous and asynchronous. """ - -ExtraHandlerBuilder: TypeAlias = Union[ - "_ComponentExtraHandler", - TypedDict( - "WrapperDict", - { - "timeout": NotRequired[Optional[float]], - "asynchronous": NotRequired[bool], - "functions": List[ExtraHandlerFunction], - }, - ), - List[ExtraHandlerFunction], -] -""" -A type, representing anything that can be transformed to ExtraHandlers. -It can be: - -- ExtraHandlerFunction object -- Dictionary, containing keys `timeout`, `asynchronous`, `functions` -""" - - -ServiceBuilder: TypeAlias = Union[ - ServiceFunction, - "Service", - str, - TypedDict( - "ServiceDict", - { - "handler": "ServiceBuilder", - "before_handler": NotRequired[Optional[ExtraHandlerBuilder]], - "after_handler": NotRequired[Optional[ExtraHandlerBuilder]], - "timeout": NotRequired[Optional[float]], - "asynchronous": NotRequired[bool], - "start_condition": NotRequired[StartConditionCheckerFunction], - "name": Optional[str], - }, - ), -] -""" -A type, representing anything that can be transformed to service. -It can be: - -- ServiceFunction (will become handler) -- Service object (will be spread and recreated) -- String 'ACTOR' - the pipeline Actor will be placed there -- Dictionary, containing keys that are present in Service constructor parameters -""" - - -ServiceGroupBuilder: TypeAlias = Union[ - List[Union[ServiceBuilder, List[ServiceBuilder], "ServiceGroup"]], - "ServiceGroup", -] -""" -A type, representing anything that can be transformed to service group. -It can be: - -- List of `ServiceBuilders`, `ServiceGroup` objects and lists (recursive) -- `ServiceGroup` object (will be spread and recreated) -""" - - -PipelineBuilder: TypeAlias = TypedDict( - "PipelineBuilder", - { - "messenger_interface": NotRequired[Optional["MessengerInterface"]], - "context_storage": NotRequired[Optional[Union["DBContextStorage", Dict]]], - "components": ServiceGroupBuilder, - "before_handler": NotRequired[Optional[ExtraHandlerBuilder]], - "after_handler": NotRequired[Optional[ExtraHandlerBuilder]], - "optimization_warnings": NotRequired[bool], - "parallelize_processing": NotRequired[bool], - "script": Union["Script", Dict], - "start_label": "NodeLabel2Type", - "fallback_label": NotRequired[Optional["NodeLabel2Type"]], - "label_priority": NotRequired[float], - "condition_handler": NotRequired[Optional[Callable]], - "handlers": NotRequired[Optional[Dict["ActorStage", List[Callable]]]], - }, -) -""" -A type, representing anything that can be transformed to pipeline. -It can be Dictionary, containing keys that are present in Pipeline constructor parameters. -""" From ace4a5f8bf8ce8f41129c73e841290442c1849f8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 10 Jul 2024 07:44:10 +0500 Subject: [PATCH 02/86] forgot to apply a few things --- chatsky/pipeline/pipeline/actor.py | 4 +++- chatsky/pipeline/service/extra.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 7fed26642..bb988a321 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -27,6 +27,7 @@ import logging import asyncio from typing import Union, Callable, Optional, Dict, List, TYPE_CHECKING +from pydantic import BaseModel, Field, model_validator import copy from chatsky.utils.turn_caching import cache_clear @@ -45,7 +46,8 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class Actor: +# arbitrary_types_allowed for testing, will remove later +class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ The class which is used to process :py:class:`~chatsky.script.Context` according to the :py:class:`~chatsky.script.Script`. diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index aedec2a16..756b3bba1 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,6 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING +from pydantic import BaseModel, computed_field from chatsky.script import Context @@ -29,7 +30,8 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class _ComponentExtraHandler: +# arbitrary_types_allowed for testing, will remove later +class _ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class, representing an extra pipeline component handler. A component extra handler is a set of functions, attached to pipeline component (before or after it). From c49fde9232f34650d8f3af650902f55c80f549e7 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 10 Jul 2024 08:30:45 +0500 Subject: [PATCH 03/86] formatted + partial self-review, feedback required --- chatsky/pipeline/pipeline/component.py | 21 ++++++++------ chatsky/pipeline/pipeline/pipeline.py | 10 +++---- chatsky/pipeline/service/extra.py | 40 +++++++++++++++++--------- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 5 ++-- chatsky/pipeline/types.py | 3 +- 6 files changed, 48 insertions(+), 33 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 14a5fc891..f4e475172 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -61,12 +61,15 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo :param path: Separated by dots path to component, is universally unique. """ + # I think before this you could pass a List[ExtraHandlerFunction] which would be turned into a ComponentExtraHandler + # Now you can't, option removed. Is that correct? Seems easy to do with field_validator. + # Possible TODO: Implement a Pydantic field_validator here for keeping that option. before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) timeout: Optional[float] = None requested_async_flag: Optional[bool] = None calculated_async_flag: bool = False - # Is the Field here correct? I'll check later. + # Is this field really Optional[]? Also, is the Field(default=) done right? start_condition: Optional[StartConditionCheckerFunction] = Field(default=always_start_condition) name: Optional[str] = None path: Optional[str] = None @@ -138,20 +141,20 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin except asyncio.TimeoutError: logger.warning(f"{type(self).__name__} '{self.name}' {extra_handler.stage} extra handler timed out!") - # Named this run_component, because ServiceGroup and Actor are components now too, - # and naming this run_service wouldn't be on point, they're not just services. - # The only problem I have is that this is kind of too generic, even confusingly generic, since _run() exists. - # Possible solution: implement _run within these classes themselves. - # My problem: centralizing Extra Handlers within PipelineComponent feels right. Why should Services have the right to run Extra Handlers however they want? They were already run there without checking the start_condition, which was a mistake. + # Named this run_component, because ServiceGroup and Actor are components now too, and naming this run_service + # wouldn't be on point, they're not just services. The only problem I have is that this is kind of too generic, + # even confusingly generic, since _run() exists. Possible solution: implement _run within these classes + # themselves. My problem: centralizing Extra Handlers within PipelineComponent feels right. Why should Services + # have the right to run Extra Handlers however they want? They were already run there without checking the + # start_condition, which was a mistake. @abstractmethod async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: raise NotImplementedError - @abc.abstractmethod async def _run(self, ctx: Context, pipeline: Pipeline) -> None: """ - A method for running a pipeline component. Executes extra handlers before and after execution, launches `run_component` method. - This method is run after the component's timeout is set (if needed). + A method for running a pipeline component. Executes extra handlers before and after execution, + launches `run_component` method. This method is run after the component's timeout is set (if needed). :param ctx: Current dialog :py:class:`~.Context`. :param pipeline: This :py:class:`~.Pipeline`. diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 5e90a1d53..d6d520c24 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -28,7 +28,7 @@ from chatsky.messengers.common import MessengerInterface from chatsky.slots.slots import GroupSlot from ..service.group import ServiceGroup -from ..service.extra import _ComponentExtraHandler +from ..service.extra import ComponentExtraHandler from ..types import ( ServiceFunction, GlobalExtraHandlerType, @@ -88,7 +88,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ - components: Union[ServiceGroup, dict, List] + components: List[PipelineComponent] script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None @@ -97,8 +97,8 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): handlers: Optional[Dict[ActorStage, List[Callable]]] = None messenger_interface: Optional[MessengerInterface] = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None - before_handler: Optional[_ComponentExtraHandler] = None - after_handler: Optional[_ComponentExtraHandler] = None + before_handler: Optional[List[ExtraHandlerFunction]] = None + after_handler: Optional[List[ExtraHandlerFunction]] = None timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False @@ -202,7 +202,7 @@ def pretty_format(self, show_extra_handlers: bool = False, indent: int = 4) -> s Resulting string structure is somewhat similar to YAML string. Should be used in debugging/logging purposes and should not be parsed. - :param show_wrappers: Whether to include Wrappers or not (could be many and/or generated). + :param show_extra_handlers: Whether to include Wrappers or not (could be many and/or generated). :param indent: Offset from new line to add before component children. """ return pretty_format_component_info_dict(self.info_dict, show_extra_handlers, indent=indent) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 756b3bba1..284b2ed0e 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -31,16 +31,15 @@ # arbitrary_types_allowed for testing, will remove later -class _ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=True): +class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class, representing an extra pipeline component handler. A component extra handler is a set of functions, attached to pipeline component (before or after it). Extra handlers should execute supportive tasks (like time or resources measurement, minor data transformations). Extra handlers should NOT edit context or pipeline, use services for that purpose instead. - :param functions: An `ExtraHandlerBuilder` object, an `_ComponentExtraHandler` instance, - a dict or a list of :py:data:`~.ExtraHandlerFunction`. - :type functions: :py:data:`~.ExtraHandlerBuilder` + :param functions: A list of :py:data:`~.ExtraHandlerFunction`. + :type functions: :py:data:`~.ExtraHandlerFunction` :param stage: An :py:class:`~.ExtraHandlerType`, specifying whether this handler will be executed before or after pipeline component. :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), @@ -48,7 +47,7 @@ class _ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed= :param asynchronous: Requested asynchronous property. """ - functions: ExtraHandlerFunction + functions: List[ExtraHandlerFunction] stage: ExtraHandlerType = ExtraHandlerType.UNDEFINED timeout: Optional[float] = None requested_async_flag: Optional[bool] = None @@ -147,13 +146,13 @@ def info_dict(self) -> dict: } -class BeforeHandler(_ComponentExtraHandler): +class BeforeHandler(ComponentExtraHandler): """ A handler for extra functions that are executed before the component's main function. - :param functions: A callable or a list of callables that will be executed + :param functions: A list of callables that will be executed before the component's main function. - :type functions: ExtraHandlerBuilder + :type functions: List[ExtraHandlerFunction] :param timeout: Optional timeout for the execution of the extra functions, in seconds. :param asynchronous: Optional flag that indicates whether the extra functions @@ -161,22 +160,33 @@ class BeforeHandler(_ComponentExtraHandler): if all the functions in this handler are asynchronous. """ + # Instead of __init__ here, this could look like a one-liner. + # The problem would be that BeforeHandlers would need to be initialized differently. + # Like BeforeHandler(functions=functions) instead of BeforeHandler(functions) + # That would break tests, tutorials and programs. + # Instead, this here could be a wrapper meant to keep the status quo. + + # The possible one-liner: + # stage: ExtraHandlerType = ExtraHandlerType.BEFORE + def __init__( self, - functions: ExtraHandlerBuilder, + functions: List[ExtraHandlerFunction], timeout: Optional[int] = None, asynchronous: Optional[bool] = None, ): - super().__init__(functions, ExtraHandlerType.BEFORE, timeout, asynchronous) + super().__init__( + functions=functions, stage=ExtraHandlerType.BEFORE, timeout=timeout, requested_async_flag=asynchronous + ) class AfterHandler(_ComponentExtraHandler): """ A handler for extra functions that are executed after the component's main function. - :param functions: A callable or a list of callables that will be executed + :param functions: A list of callables that will be executed after the component's main function. - :type functions: ExtraHandlerBuilder + :type functions: List[ExtraHandlerFunction] :param timeout: Optional timeout for the execution of the extra functions, in seconds. :param asynchronous: Optional flag that indicates whether the extra functions @@ -186,8 +196,10 @@ class AfterHandler(_ComponentExtraHandler): def __init__( self, - functions: ExtraHandlerBuilder, + functions: List[ExtraHandlerFunction], timeout: Optional[int] = None, asynchronous: Optional[bool] = None, ): - super().__init__(functions, ExtraHandlerType.AFTER, timeout, asynchronous) + super().__init__( + functions=functions, stage=ExtraHandlerType.AFTER, timeout=timeout, requested_async_flag=asynchronous + ) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 6f78f9e41..ddb39c98f 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline + # arbitrary_types_allowed for testing, will remove later class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ @@ -193,4 +194,3 @@ def info_dict(self) -> dict: representation = super(ServiceGroup, self).info_dict representation.update({"services": [service.info_dict for service in self.components]}) return representation - diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 647a9a7c3..ce1c3a21b 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -22,7 +22,7 @@ from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from ..types import ( - ServiceBuilder, + ServiceFunction, StartConditionCheckerFunction, ComponentExecutionState, ExtraHandlerType, @@ -58,7 +58,8 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ handler: ServiceFunction - # Should these be removed from the above API reference? I think they're still useful for users if included in API reference. + # Should these be removed from the above API reference? + # I think they're still useful for users if included in API reference. # before_handler: Optional[ComponentExtraHandler] = None # after_handler: Optional[ComponentExtraHandler] = None # timeout: Optional[float] = None diff --git a/chatsky/pipeline/types.py b/chatsky/pipeline/types.py index 943b7f1f1..d13ae6938 100644 --- a/chatsky/pipeline/types.py +++ b/chatsky/pipeline/types.py @@ -17,7 +17,7 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline from chatsky.pipeline.service.service import Service from chatsky.pipeline.service.group import ServiceGroup - from chatsky.pipeline.service.extra import _ComponentExtraHandler + from chatsky.pipeline.service.extra import ComponentExtraHandler from chatsky.messengers.common.interface import MessengerInterface from chatsky.context_storages import DBContextStorage from chatsky.script import Context, ActorStage, NodeLabel2Type, Script, Message @@ -178,4 +178,3 @@ class ExtraHandlerRuntimeInfo(BaseModel): Can accept current dialog context, pipeline, and current service info. Can be both synchronous and asynchronous. """ - From 957d2314af0931592920b1e766d2cc2747f9b7b8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 10 Jul 2024 09:01:15 +0500 Subject: [PATCH 04/86] formatted with poetry + minor changes --- chatsky/pipeline/__init__.py | 6 +---- chatsky/pipeline/pipeline/actor.py | 32 +++++++++++++++----------- chatsky/pipeline/pipeline/component.py | 2 +- chatsky/pipeline/pipeline/pipeline.py | 5 ++-- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/service.py | 1 + 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 4fbe2286f..5e5ed047f 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -20,14 +20,10 @@ ExtraHandlerRuntimeInfo, ExtraHandlerFunction, ServiceFunction, - ExtraHandlerBuilder, - ServiceBuilder, - ServiceGroupBuilder, - PipelineBuilder, ) from .pipeline.pipeline import Pipeline, ACTOR -from .service.extra import BeforeHandler, AfterHandler +from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from .service.group import ServiceGroup from .service.service import Service, to_service diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index bb988a321..edbd18d51 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -46,6 +46,21 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline +# Had to define this earlier, because when Pydantic starts it's __init__ it thinks of this function as +# being referenced before assignment +async def default_condition_handler( + condition: Callable, ctx: Context, pipeline: Pipeline +) -> Callable[[Context, Pipeline], bool]: + """ + The simplest and quickest condition handler for trivial condition handling returns the callable condition: + + :param condition: Condition to copy. + :param ctx: Context of current condition. + :param pipeline: Pipeline we use in this condition. + """ + return await wrap_sync_function_in_async(condition, ctx, pipeline) + + # arbitrary_types_allowed for testing, will remove later class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ @@ -75,7 +90,9 @@ class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 - condition_handler: Optional[Callable] = Field(default=default_condition_handler) + condition_handler: Optional[Callable] = default_condition_handler + # Is this Field(default=) right here? + # condition_handler: Optional[Callable] = Field(default=default_condition_handler) handlers: Optional[Dict[ActorStage, List[Callable]]] = {} _clean_turn_cache: Optional[bool] = True # Making a 'computed field' for this feels overkill, a 'private' field is probably fine? @@ -380,16 +397,3 @@ def _choose_label( else: chosen_label = self.fallback_label return chosen_label - - -async def default_condition_handler( - condition: Callable, ctx: Context, pipeline: Pipeline -) -> Callable[[Context, Pipeline], bool]: - """ - The simplest and quickest condition handler for trivial condition handling returns the callable condition: - - :param condition: Condition to copy. - :param ctx: Context of current condition. - :param pipeline: Pipeline we use in this condition. - """ - return await wrap_sync_function_in_async(condition, ctx, pipeline) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index f4e475172..615ea4788 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -147,7 +147,7 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin # themselves. My problem: centralizing Extra Handlers within PipelineComponent feels right. Why should Services # have the right to run Extra Handlers however they want? They were already run there without checking the # start_condition, which was a mistake. - @abstractmethod + @abc.abstractmethod async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: raise NotImplementedError diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index d6d520c24..d349ccc76 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -35,6 +35,7 @@ ExtraHandlerFunction, ) from .utils import finalize_service_group, pretty_format_component_info_dict +from .component import PipelineComponent from chatsky.pipeline.pipeline.actor import Actor # """ @@ -220,8 +221,8 @@ def from_script( handlers: Optional[Dict[ActorStage, List[Callable]]] = None, context_storage: Optional[Union[DBContextStorage, Dict]] = None, messenger_interface: Optional[MessengerInterface] = None, - pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] = None, - post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] = None, + pre_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, + post_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, ) -> "Pipeline": """ Pipeline script-based constructor. diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 284b2ed0e..5ec79b8dd 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -180,7 +180,7 @@ def __init__( ) -class AfterHandler(_ComponentExtraHandler): +class AfterHandler(ComponentExtraHandler): """ A handler for extra functions that are executed after the component's main function. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index ce1c3a21b..4d442e530 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -14,6 +14,7 @@ import logging import inspect from typing import Optional, TYPE_CHECKING, Union, List +from pydantic import BaseModel, model_validator from chatsky.script import Context From f3cdd5128ab61da6748301e5586caa2d3787a3c9 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 11 Jul 2024 20:19:44 +0500 Subject: [PATCH 05/86] moved state checking to component, added single_func_init to Extra Handlers back in --- chatsky/pipeline/pipeline/actor.py | 29 ++++++-------- chatsky/pipeline/pipeline/component.py | 24 ++++++++---- chatsky/pipeline/pipeline/pipeline.py | 53 +++++++++++++++++--------- chatsky/pipeline/service/extra.py | 13 +++++-- chatsky/pipeline/service/group.py | 27 +++---------- chatsky/pipeline/service/service.py | 23 ++--------- chatsky/pipeline/types.py | 11 ++---- 7 files changed, 83 insertions(+), 97 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index edbd18d51..770f64690 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -90,46 +90,39 @@ class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 - condition_handler: Optional[Callable] = default_condition_handler - # Is this Field(default=) right here? - # condition_handler: Optional[Callable] = Field(default=default_condition_handler) + condition_handler: Callable = Field(default=default_condition_handler) handlers: Optional[Dict[ActorStage, List[Callable]]] = {} _clean_turn_cache: Optional[bool] = True # Making a 'computed field' for this feels overkill, a 'private' field is probably fine? @model_validator(mode="after") def actor_validator(self): - self.script = script if isinstance(script, Script) else Script(script=script) - self.start_label = normalize_label(start_label) + if not isinstance(self.script, Script): + self.script = Script(script=self.script) + self.start_label = normalize_label(self.start_label) if self.script.get(self.start_label[0], {}).get(self.start_label[1]) is None: raise ValueError(f"Unknown start_label={self.start_label}") - if fallback_label is None: + if self.fallback_label is None: self.fallback_label = self.start_label else: - self.fallback_label = normalize_label(fallback_label) + self.fallback_label = normalize_label(self.fallback_label) if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: raise ValueError(f"Unknown fallback_label={self.fallback_label}") # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True - async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: + async def __call__(self, pipeline: Pipeline, ctx: Context): + await self.run_component(pipeline, ctx) + + async def run_component(self, pipeline: Pipeline, ctx: Context) -> None: """ Method for running an `Actor`. - Catches runtime exceptions and logs them. - :param ctx: Current dialog context. :param pipeline: Current pipeline. + :param ctx: Current dialog context. """ - try: - # This line could be: "await self(pipeline, ctx)", but I'm not sure. - await pipeline.actor(pipeline, ctx) - except Exception as exc: - self._set_state(ctx, ComponentExecutionState.FAILED) - logger.error(f"Actor '{self.name}' execution failed!", exc_info=exc) - - async def __call__(self, pipeline: Pipeline, ctx: Context): await self._run_handlers(ctx, pipeline, ActorStage.CONTEXT_INIT) # get previous node diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 615ea4788..d2bbb7d61 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -159,14 +159,22 @@ async def _run(self, ctx: Context, pipeline: Pipeline) -> None: :param ctx: Current dialog :py:class:`~.Context`. :param pipeline: This :py:class:`~.Pipeline`. """ - if await self.start_condition(ctx, pipeline): - await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) - - self._set_state(ctx, ComponentExecutionState.RUNNING) - await self.run_component(ctx, pipeline) - self._set_state(ctx, ComponentExecutionState.FINISHED) - - await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) + # In the draft there was an 'await' before the start_condition, but my IDE gives a warning about this. + # Plus, previous implementation also doesn't have an await expression there. + # Though I understand why it's a good idea. (could be some slow function) + if self.start_condition(ctx, pipeline): + try: + await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) + + self._set_state(ctx, ComponentExecutionState.RUNNING) + await self.run_component(ctx, pipeline) + if self.get_state(ctx) is not ComponentExecutionState.FAILED: + self._set_state(ctx, ComponentExecutionState.FINISHED) + + await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) + except Exception as exc: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"Service '{self.name}' execution failed!", exc_info=exc) else: self._set_state(ctx, ComponentExecutionState.NOT_RUN) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index d349ccc76..bca298dd2 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -17,7 +17,7 @@ import asyncio import logging from typing import Union, List, Dict, Optional, Hashable, Callable -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, computed_field from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -89,7 +89,9 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ - components: List[PipelineComponent] + # If Actor is passed here, program will break. Should Actor be a private class somehow? + pre_services: List[PipelineComponent] + post_services: List[PipelineComponent] script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None @@ -108,29 +110,35 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): _services_pipeline: Optional[ServiceGroup] _clean_turn_cache: Optional[bool] - @model_validator(mode="after") - def pipeline_init(self): - self.actor = None - self._services_pipeline = ServiceGroup( - components=self.components, + @computed_field(alias="_services_pipeline", repr=False) + def create_main_service_group(self) -> ServiceGroup: + components = [self.pre_services, self.actor, self.post_services] + services_pipeline = ServiceGroup( + components=components, before_handler=self.before_handler, after_handler=self.after_handler, timeout=self.timeout, ) - self._services_pipeline.name = "pipeline" - self._services_pipeline.path = ".pipeline" + services_pipeline.name = "pipeline" + services_pipeline.path = ".pipeline" + return services_pipeline + + @model_validator(mode="after") + def pipeline_init(self): + # Here Actor can be initialized, pretty sure. + self.set_actor( + self.script, + self.start_label, + self.fallback_label, + self.label_priority, + self.condition_handler, + self.handlers, + ) + # finalize_service_group() needs to have the search for Actor removed. + # Though this should work too. actor_exists = finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) if not actor_exists: raise Exception("Actor not found in the pipeline!") - else: - self.set_actor( - self.script, - self.start_label, - self.fallback_label, - self.label_priority, - self.condition_handler, - self.handlers, - ) if self.actor is None: raise Exception("Actor wasn't initialized correctly!") @@ -303,7 +311,14 @@ def set_actor( - key :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - value List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. """ - self.actor = Actor(script, start_label, fallback_label, label_priority, condition_handler, handlers) + self.actor = Actor( + script=script, + start_label=start_label, + fallback_label=fallback_label, + label_priority=label_priority, + condition_handler=condition_handler, + handlers=handlers, + ) @classmethod def from_dict(cls, dictionary: PipelineBuilder) -> "Pipeline": diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 5ec79b8dd..23192f266 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,12 +10,11 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING -from pydantic import BaseModel, computed_field +from typing import Optional, List, TYPE_CHECKING, Any +from pydantic import BaseModel, computed_field, field_validator from chatsky.script import Context -from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from ..types import ( ServiceRuntimeInfo, @@ -52,6 +51,14 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T timeout: Optional[float] = None requested_async_flag: Optional[bool] = None + @field_validator("functions") + @classmethod + # Note to self: Here Script class has "@validate_call". Is it needed here? + def single_handler_init(cls, func: Any): + if isinstance(func, ExtraHandlerFunction): + return [func] + return func + @computed_field(alias="calculated_async_flag", repr=False) def calculate_async_flag(self) -> bool: return all([asyncio.iscoroutinefunction(func) for func in self.functions]) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index ddb39c98f..f007f1135 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -74,14 +74,17 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr def calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) - async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ - Method for running this service group. + Method for running this service group. Catches runtime exceptions and logs them. It doesn't include extra handlers execution, start condition checking or error handling - pure execution only. Executes components inside the group based on its `asynchronous` property. Collects information about their execution state - group is finished successfully only if all components in it finished successfully. + :param ctx: Current dialog context. + :param pipeline: The current pipeline. + :param ctx: Current dialog context. :param pipeline: The current pipeline. """ @@ -100,29 +103,9 @@ async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> None: if service.asynchronous and isinstance(service_result, Awaitable): await service_result - # This gets overwritten with "FINISHED" at PipelineComponent.run(). - # TODO: resolve this conflict. failed = any([service.get_state(ctx) == ComponentExecutionState.FAILED for service in self.components]) self._set_state(ctx, ComponentExecutionState.FAILED if failed else ComponentExecutionState.FINISHED) - async def run_component( - self, - ctx: Context, - pipeline: Pipeline, - ) -> None: - """ - Method for handling this group execution. - Catches runtime exceptions and logs them. - - :param ctx: Current dialog context. - :param pipeline: The current pipeline. - """ - try: - await self._run_services_group(ctx, pipeline) - except Exception as exc: - self._set_state(ctx, ComponentExecutionState.FAILED) - logger.error(f"ServiceGroup '{self.name}' execution failed!", exc_info=exc) - def log_optimization_warnings(self): """ Method for logging service group optimization warnings for all this groups inner components. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 4d442e530..54ca40d2e 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -14,7 +14,7 @@ import logging import inspect from typing import Optional, TYPE_CHECKING, Union, List -from pydantic import BaseModel, model_validator +from pydantic import model_validator from chatsky.script import Context @@ -73,11 +73,10 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): def tick_async_flag(self): self.calculated_async_flag = True - async def _run_handler(self, ctx: Context, pipeline: Pipeline) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ - Method for service `handler` execution. - Handler has three possible signatures, so this method picks the right one to invoke. - These possible signatures are: + Method for running this service. Service 'handler' has three possible signatures, + so this method picks the right one to invoke. These possible signatures are: - (ctx: Context) - accepts current dialog context only. - (ctx: Context, pipeline: Pipeline) - accepts context and current pipeline. @@ -98,20 +97,6 @@ async def _run_handler(self, ctx: Context, pipeline: Pipeline) -> None: else: raise Exception(f"Too many parameters required for service '{self.name}' handler: {handler_params}!") - async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: - """ - Method for running this service. - Catches runtime exceptions and logs them. - - :param ctx: Current dialog context. - :param pipeline: Current pipeline. - """ - try: - await self._run_handler(ctx, pipeline) - except Exception as exc: - self._set_state(ctx, ComponentExecutionState.FAILED) - logger.error(f"Service '{self.name}' execution failed!", exc_info=exc) - @property def info_dict(self) -> dict: """ diff --git a/chatsky/pipeline/types.py b/chatsky/pipeline/types.py index d13ae6938..56e955855 100644 --- a/chatsky/pipeline/types.py +++ b/chatsky/pipeline/types.py @@ -8,19 +8,14 @@ from __future__ import annotations from enum import unique, Enum -from typing import Callable, Union, Awaitable, Dict, List, Optional, Iterable, Any, Protocol, Hashable, TYPE_CHECKING -from typing_extensions import NotRequired, TypedDict, TypeAlias +from typing import Callable, Union, Awaitable, Dict, Optional, Iterable, Any, Protocol, Hashable, TYPE_CHECKING +from typing_extensions import TypeAlias from pydantic import BaseModel if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline - from chatsky.pipeline.service.service import Service - from chatsky.pipeline.service.group import ServiceGroup - from chatsky.pipeline.service.extra import ComponentExtraHandler - from chatsky.messengers.common.interface import MessengerInterface - from chatsky.context_storages import DBContextStorage - from chatsky.script import Context, ActorStage, NodeLabel2Type, Script, Message + from chatsky.script import Context, Message class PipelineRunnerFunction(Protocol): From 0e21a3e201c9676c35fe79a0a581c15ea9ca46c3 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 12 Jul 2024 17:53:59 +0500 Subject: [PATCH 06/86] field_validator added to Service, minor type changes --- chatsky/pipeline/pipeline/pipeline.py | 4 ++-- chatsky/pipeline/service/extra.py | 4 ++-- chatsky/pipeline/service/group.py | 14 +++++++++++--- chatsky/pipeline/service/service.py | 11 +++++------ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index bca298dd2..8484302ff 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -98,7 +98,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): label_priority: float = 1.0 condition_handler: Optional[Callable] = None handlers: Optional[Dict[ActorStage, List[Callable]]] = None - messenger_interface: Optional[MessengerInterface] = Field(default_factory=CLIMessengerInterface) + messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None before_handler: Optional[List[ExtraHandlerFunction]] = None after_handler: Optional[List[ExtraHandlerFunction]] = None @@ -321,7 +321,7 @@ def set_actor( ) @classmethod - def from_dict(cls, dictionary: PipelineBuilder) -> "Pipeline": + def from_dict(cls, dictionary: dict) -> "Pipeline": """ Pipeline dictionary-based constructor. Dictionary should have the fields defined in Pipeline main constructor, diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 23192f266..c8b8448b6 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -37,13 +37,13 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T Extra handlers should execute supportive tasks (like time or resources measurement, minor data transformations). Extra handlers should NOT edit context or pipeline, use services for that purpose instead. - :param functions: A list of :py:data:`~.ExtraHandlerFunction`. + :param functions: A list or instance of :py:data:`~.ExtraHandlerFunction`. :type functions: :py:data:`~.ExtraHandlerFunction` :param stage: An :py:class:`~.ExtraHandlerType`, specifying whether this handler will be executed before or after pipeline component. :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), if it exceeds this time, it is interrupted. - :param asynchronous: Requested asynchronous property. + :param requested_async_flag: Requested asynchronous property. """ functions: List[ExtraHandlerFunction] diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index f007f1135..f0d793201 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,8 +11,8 @@ from __future__ import annotations import asyncio import logging -from typing import Optional, List, Union, Awaitable, TYPE_CHECKING -from pydantic import model_validator +from typing import Optional, List, Union, Awaitable, TYPE_CHECKING, Any +from pydantic import model_validator, field_validator from chatsky.script import Context from .extra import ComponentExtraHandler @@ -51,7 +51,7 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr :param after_handler: List of `_ComponentExtraHandler` to add to the group. :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] :param timeout: Timeout to add to the group. - :param asynchronous: Requested asynchronous property. + :param requested_async_flag: Requested asynchronous property. :param start_condition: :py:data:`~.StartConditionCheckerFunction` that is invoked before each group execution; group is executed only if it returns `True`. :param name: Requested group name. @@ -67,6 +67,14 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr # start_condition: Optional[StartConditionCheckerFunction] = None # name: Optional[str] = None + @field_validator("functions") + @classmethod + # Note to self: Here Script class has "@validate_call". Is it needed here? + def single_component_init(cls, comp: Any): + if isinstance(comp, PipelineComponent): + return [comp] + return comp + # Is there a better way to do this? calculated_async_flag is exposed to the user right now. # Of course, they might not want to break their own program, but what if. # Maybe I could just make this a 'private' field, like '_calc_async' diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 54ca40d2e..3697f818a 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -51,7 +51,7 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): :param after_handler: List of `_ComponentExtraHandler` to add to the group. :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] :param timeout: Timeout to add to the group. - :param asynchronous: Requested asynchronous property. + :param requested_async_flag: Requested asynchronous property. :param start_condition: StartConditionCheckerFunction that is invoked before each service execution; service is executed only if it returns `True`. :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`] @@ -65,7 +65,6 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): # after_handler: Optional[ComponentExtraHandler] = None # timeout: Optional[float] = None # asynchronous: Optional[bool] = None - calculated_async_flag: Optional[bool] = True # start_condition: Optional[StartConditionCheckerFunction] = None # name: Optional[str] = None @@ -113,10 +112,10 @@ def info_dict(self) -> dict: representation.update({"handler": service_representation}) return representation - +# If this function will continue existing. def to_service( - before_handler: Optional[ExtraHandlerBuilder] = None, - after_handler: Optional[ExtraHandlerBuilder] = None, + before_handler: Optional[ComponentExtraHandler] = None, + after_handler: Optional[ComponentExtraHandler] = None, timeout: Optional[int] = None, asynchronous: Optional[bool] = None, start_condition: Optional[StartConditionCheckerFunction] = None, @@ -128,7 +127,7 @@ def to_service( All arguments are passed directly to `Service` constructor. """ - def inner(handler: ServiceBuilder) -> Service: + def inner(handler: ServiceFunction) -> Service: return Service( handler=handler, before_handler=before_handler, From 9e5b6881749f27e988dae557ad66a4c07ce7913b Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 15 Jul 2024 08:25:36 +0500 Subject: [PATCH 07/86] Several changes and fixes. Tests compile now, but 21 of them fail. --- chatsky/__rebuild_pydantic_models__.py | 14 ++- chatsky/pipeline/pipeline/actor.py | 11 ++- chatsky/pipeline/pipeline/component.py | 2 - chatsky/pipeline/pipeline/pipeline.py | 128 +++++++++++++++---------- chatsky/pipeline/pipeline/utils.py | 91 ++++-------------- chatsky/pipeline/service/extra.py | 12 +-- chatsky/pipeline/service/group.py | 26 +++-- chatsky/pipeline/service/service.py | 15 +-- 8 files changed, 134 insertions(+), 165 deletions(-) diff --git a/chatsky/__rebuild_pydantic_models__.py b/chatsky/__rebuild_pydantic_models__.py index 6d4c5dd92..80716816e 100644 --- a/chatsky/__rebuild_pydantic_models__.py +++ b/chatsky/__rebuild_pydantic_models__.py @@ -1,9 +1,19 @@ # flake8: noqa: F401 -from chatsky.pipeline import Pipeline +from chatsky.pipeline import Pipeline, Service, ServiceGroup, ComponentExtraHandler +from chatsky.pipeline.pipeline.actor import Actor +from chatsky.pipeline.pipeline.component import PipelineComponent from chatsky.pipeline.types import ExtraHandlerRuntimeInfo from chatsky.script import Context, Script - +""" +Actor.model_rebuild() +PipelineComponent.model_rebuild() +ComponentExtraHandler.model_rebuild() +Pipeline.model_rebuild() +Service.model_rebuild() +ServiceGroup.model_rebuild() +""" +Pipeline.model_rebuild() Script.model_rebuild() Context.model_rebuild() ExtraHandlerRuntimeInfo.model_rebuild() diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 770f64690..ad17d4f17 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -27,9 +27,10 @@ import logging import asyncio from typing import Union, Callable, Optional, Dict, List, TYPE_CHECKING -from pydantic import BaseModel, Field, model_validator +from pydantic import Field, model_validator import copy +from chatsky.pipeline.pipeline.component import PipelineComponent from chatsky.utils.turn_caching import cache_clear from chatsky.script.core.types import ActorStage, NodeLabel2Type, NodeLabel3Type, LabelType from chatsky.script.core.message import Message @@ -62,7 +63,7 @@ async def default_condition_handler( # arbitrary_types_allowed for testing, will remove later -class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): +class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ The class which is used to process :py:class:`~chatsky.script.Context` according to the :py:class:`~chatsky.script.Script`. @@ -95,6 +96,12 @@ class Actor(BaseModel, extra="forbid", arbitrary_types_allowed=True): _clean_turn_cache: Optional[bool] = True # Making a 'computed field' for this feels overkill, a 'private' field is probably fine? + # Basically, Actor cannot be async with other components. That is correct, yes? + @model_validator(mode="after") + def tick_async_flag(self): + self.calculated_async_flag = False + return self + @model_validator(mode="after") def actor_validator(self): if not isinstance(self.script, Script): diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index d2bbb7d61..a66823064 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -81,8 +81,6 @@ def pipeline_component_validator(self): if self.name is not None and (self.name == "" or "." in self.name): raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {self.name})!") - self.calculated_async_flag = all([service.asynchronous for service in self.components]) - if not self.calculated_async_flag and self.requested_async_flag: raise Exception(f"{type(self).__name__} '{self.name}' can't be asynchronous!") return self diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 8484302ff..700d340fb 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -16,8 +16,8 @@ import asyncio import logging -from typing import Union, List, Dict, Optional, Hashable, Callable -from pydantic import BaseModel, Field, model_validator, computed_field +from typing import Union, List, Dict, Optional, Hashable, Callable, Any +from pydantic import BaseModel, Field, model_validator, computed_field, field_validator from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -27,30 +27,33 @@ from chatsky.messengers.console import CLIMessengerInterface from chatsky.messengers.common import MessengerInterface from chatsky.slots.slots import GroupSlot -from ..service.group import ServiceGroup -from ..service.extra import ComponentExtraHandler +from chatsky.pipeline.service.service import Service +from chatsky.pipeline.service.group import ServiceGroup +from chatsky.pipeline.service.extra import BeforeHandler, AfterHandler from ..types import ( ServiceFunction, GlobalExtraHandlerType, ExtraHandlerFunction, + # Everything breaks without this import, even though it's unused. + # Should it go into TYPE_CHECKING? Or what should be done? + StartConditionCheckerFunction, ) -from .utils import finalize_service_group, pretty_format_component_info_dict -from .component import PipelineComponent -from chatsky.pipeline.pipeline.actor import Actor - -# """ -# Debug code. No need to look here. -# from ..service.group import Service -# from chatsky.pipeline.service.extra import _ComponentExtraHandler -# """ +from .utils import finalize_service_group +from chatsky.pipeline.pipeline.actor import Actor, default_condition_handler +""" +if TYPE_CHECKING: + from .. import Service + from ..service.group import ServiceGroup +""" logger = logging.getLogger(__name__) ACTOR = "ACTOR" # Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. -class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): +# Must also add back in 'extra="forbid"', removed for testing. +class Pipeline(BaseModel, arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: @@ -89,9 +92,9 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ - # If Actor is passed here, program will break. Should Actor be a private class somehow? - pre_services: List[PipelineComponent] - post_services: List[PipelineComponent] + # I wonder what happens/should happen here if only one callable is passed. + pre_services: Optional[List[Union[Service, ServiceGroup]]] = [] + post_services: Optional[List[Union[Service, ServiceGroup]]] = [] script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None @@ -100,45 +103,81 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): handlers: Optional[Dict[ActorStage, List[Callable]]] = None messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None - before_handler: Optional[List[ExtraHandlerFunction]] = None - after_handler: Optional[List[ExtraHandlerFunction]] = None + before_handler: Optional[List[ExtraHandlerFunction]] = [] + after_handler: Optional[List[ExtraHandlerFunction]] = [] timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False - # TO-DO: Remove/change three parameters below (if possible) - actor: Optional[Actor] = None + # TO-DO: Remove/change parameters below (if possible) _services_pipeline: Optional[ServiceGroup] _clean_turn_cache: Optional[bool] @computed_field(alias="_services_pipeline", repr=False) - def create_main_service_group(self) -> ServiceGroup: - components = [self.pre_services, self.actor, self.post_services] + def _services_pipeline(self) -> ServiceGroup: + components = [*self.pre_services, self.actor, *self.post_services] services_pipeline = ServiceGroup( components=components, - before_handler=self.before_handler, - after_handler=self.after_handler, + before_handler=BeforeHandler(self.before_handler), + after_handler=AfterHandler(self.after_handler), timeout=self.timeout, ) services_pipeline.name = "pipeline" services_pipeline.path = ".pipeline" return services_pipeline + @computed_field(repr=False) + def actor(self) -> Actor: + return Actor( + script=self.script, + start_label=self.start_label, + fallback_label=self.fallback_label, + label_priority=self.label_priority, + condition_handler=self.condition_handler, + handlers=self.handlers, + ) + + @field_validator("before_handler") + @classmethod + def single_before_handler_init(cls, handler: Any): + if isinstance(handler, ExtraHandlerFunction): + return [handler] + return handler + + @field_validator("after_handler") + @classmethod + def single_after_handler_init(cls, handler: Any): + if isinstance(handler, ExtraHandlerFunction): + return [handler] + return handler + + # This looks kind of terrible. I could remove this and ask the user to do things the right way, + # but this just seems more convenient for the user. Like, "put just one callable in pre-services"? Done. + # TODO: Change this to a large model_validator(mode="before") for less code bloat + @field_validator("pre_services") + @classmethod + def single_pre_service_init(cls, services: Any): + if not isinstance(services, List): + return [services] + return services + + @field_validator("post_services") + @classmethod + def single_post_service_init(cls, services: Any): + if not isinstance(services, List): + return [services] + return services + @model_validator(mode="after") def pipeline_init(self): - # Here Actor can be initialized, pretty sure. - self.set_actor( - self.script, - self.start_label, - self.fallback_label, - self.label_priority, - self.condition_handler, - self.handlers, - ) + """# I wonder if I could make actor itself a @computed_field, but I'm not sure that would work. + # What if the cache gets cleaned at some point? Then a new Actor would be created. + # Same goes for @cached_property. Would @property work? + self.actor = self._set_actor""" + # finalize_service_group() needs to have the search for Actor removed. # Though this should work too. - actor_exists = finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) - if not actor_exists: - raise Exception("Actor not found in the pipeline!") + finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) + # This could be removed. if self.actor is None: raise Exception("Actor wasn't initialized correctly!") @@ -205,17 +244,6 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } - def pretty_format(self, show_extra_handlers: bool = False, indent: int = 4) -> str: - """ - Method for receiving pretty-formatted string description of the pipeline. - Resulting string structure is somewhat similar to YAML string. - Should be used in debugging/logging purposes and should not be parsed. - - :param show_extra_handlers: Whether to include Wrappers or not (could be many and/or generated). - :param indent: Offset from new line to add before component children. - """ - return pretty_format_component_info_dict(self.info_dict, show_extra_handlers, indent=indent) - @classmethod def from_script( cls, @@ -223,12 +251,12 @@ def from_script( start_label: NodeLabel2Type, fallback_label: Optional[NodeLabel2Type] = None, label_priority: float = 1.0, - condition_handler: Optional[Callable] = None, + condition_handler: Optional[Callable] = default_condition_handler, slots: Optional[Union[GroupSlot, Dict]] = None, parallelize_processing: bool = False, handlers: Optional[Dict[ActorStage, List[Callable]]] = None, context_storage: Optional[Union[DBContextStorage, Dict]] = None, - messenger_interface: Optional[MessengerInterface] = None, + messenger_interface: Optional[MessengerInterface] = CLIMessengerInterface(), pre_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, post_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, ) -> "Pipeline": diff --git a/chatsky/pipeline/pipeline/utils.py b/chatsky/pipeline/pipeline/utils.py index 752bde18c..fab203833 100644 --- a/chatsky/pipeline/pipeline/utils.py +++ b/chatsky/pipeline/pipeline/utils.py @@ -6,86 +6,46 @@ """ import collections -from typing import Union, List +from typing import List from inspect import isfunction +from .actor import Actor +from .component import PipelineComponent from ..service.service import Service from ..service.group import ServiceGroup -def pretty_format_component_info_dict( - service: dict, - show_extra_handlers: bool, - offset: str = "", - extra_handlers_key: str = "extra_handlers", - type_key: str = "type", - name_key: str = "name", - indent: int = 4, -) -> str: - """ - Function for dumping any pipeline components info dictionary (received from `info_dict` property) as a string. - Resulting string is formatted with YAML-like format, however it's not strict and shouldn't be parsed. - However, most preferable usage is via `pipeline.pretty_format`. - - :param service: (required) Pipeline components info dictionary. - :param show_extra_handlers: (required) Whether to include Extra Handlers or not (could be many and/or generated). - :param offset: Current level new line offset. - :param extra_handlers_key: Key that is mapped to Extra Handlers lists. - :param type_key: Key that is mapped to components type name. - :param name_key: Key that is mapped to components name. - :param indent: Current level new line offset (whitespace number). - :return: Formatted string - """ - indent = " " * indent - representation = f"{offset}{service.get(type_key, '[None]')}%s:\n" % ( - f" '{service.get(name_key, '[None]')}'" if name_key in service else "" - ) - for key, value in service.items(): - if key not in (type_key, name_key, extra_handlers_key) or (key == extra_handlers_key and show_extra_handlers): - if isinstance(value, List): - if len(value) > 0: - values = [ - pretty_format_component_info_dict(instance, show_extra_handlers, f"{indent * 2}{offset}") - for instance in value - ] - value_str = "\n%s" % "\n".join(values) - else: - value_str = "[None]" - else: - value_str = str(value) - representation += f"{offset}{indent}{key}: {value_str}\n" - return representation[:-1] - - def rename_component_incrementing( - service: Union[Service, ServiceGroup], collisions: List[Union[Service, ServiceGroup]] + component: PipelineComponent, collisions: List[PipelineComponent] ) -> str: """ Function for generating new name for a pipeline component, that has similar name with other components in the same group. The name is generated according to these rules: - - If service's handler is "ACTOR", it is named `actor`. - - If service's handler is `Callable`, it is named after this `callable`. + - If component is an `Actor`, it is named `actor`. + - If component is a `Service` and the service's handler is `Callable`, it is named after this `callable`. - If it's a service group, it is named `service_group`. - Otherwise, it is names `noname_service`. - | After that, `_[NUMBER]` is added to the resulting name, where `_[NUMBER]` is number of components with the same name in current service group. - :param service: Service to be renamed. - :param collisions: Services in the same service group as service. + :param component: Component to be renamed. + :param collisions: Components in the same service group as component. :return: Generated name """ - if isinstance(service, Service) and isinstance(service.handler, str) and service.handler == "ACTOR": + if isinstance(component, Actor): base_name = "actor" - elif isinstance(service, Service) and callable(service.handler): - if isfunction(service.handler): - base_name = service.handler.__name__ + # Pretty sure that service.handler can only be a callable now. Should the newly irrelevant logic be removed? + elif isinstance(component, Service) and callable(component.handler): + if isfunction(component.handler): + base_name = component.handler.__name__ else: - base_name = service.handler.__class__.__name__ - elif isinstance(service, ServiceGroup): + base_name = component.handler.__class__.__name__ + elif isinstance(component, ServiceGroup): base_name = "service_group" else: + # This should never be triggered. (All PipelineComponent derivatives are handled above) base_name = "noname_service" name_index = 0 @@ -94,19 +54,17 @@ def rename_component_incrementing( return f"{base_name}_{name_index}" -def finalize_service_group(service_group: ServiceGroup, path: str = ".") -> bool: +def finalize_service_group(service_group: ServiceGroup, path: str = ".") -> None: """ Function that iterates through a service group (and all its subgroups), finalizing component's names and paths in it. Components are renamed only if user didn't set a name for them. Their paths are also generated here. - It also searches for "ACTOR" in the group, throwing exception if no actor or multiple actors found. :param service_group: Service group to resolve name collisions in. :param path: A prefix for component paths -- path of `component` is equal to `{path}.{component.name}`. Defaults to ".". """ - actor = False names_counter = collections.Counter([component.name for component in service_group.components]) for component in service_group.components: if component.name is None: @@ -115,16 +73,5 @@ def finalize_service_group(service_group: ServiceGroup, path: str = ".") -> bool raise Exception(f"User defined service name collision ({path})!") component.path = f"{path}.{component.name}" - if isinstance(component, Service) and isinstance(component.handler, str) and component.handler == "ACTOR": - actor_found = True - elif isinstance(component, ServiceGroup): - actor_found = finalize_service_group(component, f"{path}.{component.name}") - else: - actor_found = False - - if actor_found: - if not actor: - actor = actor_found - else: - raise Exception(f"More than one actor found in group ({path})!") - return actor + if isinstance(component, ServiceGroup): + finalize_service_group(component, f"{path}.{component.name}") diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index c8b8448b6..ae0b0fc9c 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,8 +10,8 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING, Any -from pydantic import BaseModel, computed_field, field_validator +from typing import Optional, List, TYPE_CHECKING +from pydantic import BaseModel, computed_field from chatsky.script import Context @@ -51,14 +51,6 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T timeout: Optional[float] = None requested_async_flag: Optional[bool] = None - @field_validator("functions") - @classmethod - # Note to self: Here Script class has "@validate_call". Is it needed here? - def single_handler_init(cls, func: Any): - if isinstance(func, ExtraHandlerFunction): - return [func] - return func - @computed_field(alias="calculated_async_flag", repr=False) def calculate_async_flag(self) -> bool: return all([asyncio.iscoroutinefunction(func) for func in self.functions]) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index f0d793201..735f5765a 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -16,6 +16,7 @@ from chatsky.script import Context from .extra import ComponentExtraHandler +from ..pipeline.actor import Actor from ..pipeline.component import PipelineComponent from ..types import ( @@ -34,6 +35,8 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline +# I think it's fine calling this a `Service` group, even though really it's a `PipelineComponent` group. +# The user only sees this as a `Service` group like they should. # arbitrary_types_allowed for testing, will remove later class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ @@ -59,17 +62,9 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr components: List[PipelineComponent] - # Should these be removed from API reference? - # before_handler: Optional[ComponentExtraHandler] = None - # after_handler: Optional[ComponentExtraHandler] = None - # timeout: Optional[float] = None - # asynchronous: Optional[bool] = None - # start_condition: Optional[StartConditionCheckerFunction] = None - # name: Optional[str] = None - - @field_validator("functions") - @classmethod # Note to self: Here Script class has "@validate_call". Is it needed here? + @field_validator('components') + @classmethod def single_component_init(cls, comp: Any): if isinstance(comp, PipelineComponent): return [comp] @@ -78,9 +73,10 @@ def single_component_init(cls, comp: Any): # Is there a better way to do this? calculated_async_flag is exposed to the user right now. # Of course, they might not want to break their own program, but what if. # Maybe I could just make this a 'private' field, like '_calc_async' - @model_validator(mode="after") + @model_validator(mode='after') def calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) + return self async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ @@ -153,7 +149,7 @@ def add_extra_handler( self, global_extra_handler_type: GlobalExtraHandlerType, extra_handler: ExtraHandlerFunction, - condition: ExtraHandlerConditionFunction = lambda _: True, + condition: ExtraHandlerConditionFunction = lambda _: False, ): """ Method for adding a global extra handler to this group. @@ -171,10 +167,10 @@ def add_extra_handler( for service in self.components: if not condition(service.path): continue - if isinstance(service, Service): - service.add_extra_handler(global_extra_handler_type, extra_handler) - else: + if isinstance(service, ServiceGroup): service.add_extra_handler(global_extra_handler_type, extra_handler, condition) + else: + service.add_extra_handler(global_extra_handler_type, extra_handler) @property def info_dict(self) -> dict: diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 3697f818a..fd67fe9f8 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,20 +13,17 @@ from __future__ import annotations import logging import inspect -from typing import Optional, TYPE_CHECKING, Union, List +from typing import Optional, TYPE_CHECKING from pydantic import model_validator from chatsky.script import Context from .extra import ComponentExtraHandler -from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from ..types import ( ServiceFunction, StartConditionCheckerFunction, - ComponentExecutionState, - ExtraHandlerType, ) from ..pipeline.component import PipelineComponent @@ -59,18 +56,11 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ handler: ServiceFunction - # Should these be removed from the above API reference? - # I think they're still useful for users if included in API reference. - # before_handler: Optional[ComponentExtraHandler] = None - # after_handler: Optional[ComponentExtraHandler] = None - # timeout: Optional[float] = None - # asynchronous: Optional[bool] = None - # start_condition: Optional[StartConditionCheckerFunction] = None - # name: Optional[str] = None @model_validator(mode="after") def tick_async_flag(self): self.calculated_async_flag = True + return self async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ @@ -112,6 +102,7 @@ def info_dict(self) -> dict: representation.update({"handler": service_representation}) return representation + # If this function will continue existing. def to_service( before_handler: Optional[ComponentExtraHandler] = None, From 3d6e29f9b98669fac9af6b6a8595a49225af852b Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 15 Jul 2024 08:44:00 +0500 Subject: [PATCH 08/86] fixed small mistake, tests still don't pass --- chatsky/pipeline/pipeline/pipeline.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 700d340fb..45e886d58 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -35,7 +35,8 @@ GlobalExtraHandlerType, ExtraHandlerFunction, # Everything breaks without this import, even though it's unused. - # Should it go into TYPE_CHECKING? Or what should be done? + # (It says it's not defined or something like that) + # Should it go into TYPE_CHECKING? If not, what should be done? StartConditionCheckerFunction, ) from .utils import finalize_service_group @@ -244,6 +245,7 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } + # I know this function will be removed, but for testing I'll keep it for now @classmethod def from_script( cls, @@ -257,8 +259,8 @@ def from_script( handlers: Optional[Dict[ActorStage, List[Callable]]] = None, context_storage: Optional[Union[DBContextStorage, Dict]] = None, messenger_interface: Optional[MessengerInterface] = CLIMessengerInterface(), - pre_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, - post_services: Optional[List[Union[ServiceFunction, ServiceGroup]]] = None, + pre_services: Optional[List[Union[Service, ServiceGroup]]] = None, + post_services: Optional[List[Union[Service, ServiceGroup]]] = None, ) -> "Pipeline": """ Pipeline script-based constructor. @@ -298,6 +300,8 @@ def from_script( pre_services = [] if pre_services is None else pre_services post_services = [] if post_services is None else post_services return cls( + pre_services=pre_services, + post_services=post_services, script=script, start_label=start_label, fallback_label=fallback_label, @@ -308,7 +312,6 @@ def from_script( handlers=handlers, messenger_interface=messenger_interface, context_storage=context_storage, - components=[*pre_services, ACTOR, *post_services], ) def set_actor( From d92292b7fb41eb0e5b5df8f3895726e080a1b529 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 17 Jul 2024 21:29:34 +0500 Subject: [PATCH 09/86] formatted with poetry --- chatsky/__rebuild_pydantic_models__.py | 1 + chatsky/pipeline/pipeline/pipeline.py | 2 +- chatsky/pipeline/pipeline/utils.py | 4 +--- chatsky/pipeline/service/group.py | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/chatsky/__rebuild_pydantic_models__.py b/chatsky/__rebuild_pydantic_models__.py index 80716816e..a5031d859 100644 --- a/chatsky/__rebuild_pydantic_models__.py +++ b/chatsky/__rebuild_pydantic_models__.py @@ -5,6 +5,7 @@ from chatsky.pipeline.pipeline.component import PipelineComponent from chatsky.pipeline.types import ExtraHandlerRuntimeInfo from chatsky.script import Context, Script + """ Actor.model_rebuild() PipelineComponent.model_rebuild() diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 45e886d58..a2b65b4cf 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -54,7 +54,7 @@ # Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. # Must also add back in 'extra="forbid"', removed for testing. -class Pipeline(BaseModel, arbitrary_types_allowed=True): +class Pipeline(BaseModel, arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: diff --git a/chatsky/pipeline/pipeline/utils.py b/chatsky/pipeline/pipeline/utils.py index fab203833..35d6de23d 100644 --- a/chatsky/pipeline/pipeline/utils.py +++ b/chatsky/pipeline/pipeline/utils.py @@ -15,9 +15,7 @@ from ..service.group import ServiceGroup -def rename_component_incrementing( - component: PipelineComponent, collisions: List[PipelineComponent] -) -> str: +def rename_component_incrementing(component: PipelineComponent, collisions: List[PipelineComponent]) -> str: """ Function for generating new name for a pipeline component, that has similar name with other components in the same group. diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 735f5765a..cf0ead616 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -63,7 +63,7 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr components: List[PipelineComponent] # Note to self: Here Script class has "@validate_call". Is it needed here? - @field_validator('components') + @field_validator("components") @classmethod def single_component_init(cls, comp: Any): if isinstance(comp, PipelineComponent): @@ -73,7 +73,7 @@ def single_component_init(cls, comp: Any): # Is there a better way to do this? calculated_async_flag is exposed to the user right now. # Of course, they might not want to break their own program, but what if. # Maybe I could just make this a 'private' field, like '_calc_async' - @model_validator(mode='after') + @model_validator(mode="after") def calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) return self From 7ba3ac42addf44a2b25f57a99e3a0c434dccb609 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 17 Jul 2024 21:32:04 +0500 Subject: [PATCH 10/86] starting removal of pipeline.from_script() --- chatsky/pipeline/pipeline/pipeline.py | 69 --------------------------- 1 file changed, 69 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index a2b65b4cf..6aa347c27 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -245,75 +245,6 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } - # I know this function will be removed, but for testing I'll keep it for now - @classmethod - def from_script( - cls, - script: Union[Script, Dict], - start_label: NodeLabel2Type, - fallback_label: Optional[NodeLabel2Type] = None, - label_priority: float = 1.0, - condition_handler: Optional[Callable] = default_condition_handler, - slots: Optional[Union[GroupSlot, Dict]] = None, - parallelize_processing: bool = False, - handlers: Optional[Dict[ActorStage, List[Callable]]] = None, - context_storage: Optional[Union[DBContextStorage, Dict]] = None, - messenger_interface: Optional[MessengerInterface] = CLIMessengerInterface(), - pre_services: Optional[List[Union[Service, ServiceGroup]]] = None, - post_services: Optional[List[Union[Service, ServiceGroup]]] = None, - ) -> "Pipeline": - """ - Pipeline script-based constructor. - It creates :py:class:`~.Actor` object and wraps it with pipeline. - NB! It is generally not designed for projects with complex structure. - :py:class:`~.Service` and :py:class:`~.ServiceGroup` customization - becomes not as obvious as it could be with it. - Should be preferred for simple workflows with Actor auto-execution. - - :param script: (required) A :py:class:`~.Script` instance (object or dict). - :param start_label: (required) Actor start label. - :param fallback_label: Actor fallback label. - :param label_priority: Default priority value for all actor :py:const:`labels ` - where there is no priority. Defaults to `1.0`. - :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. - :param slots: Slots configuration. - :param parallelize_processing: This flag determines whether or not the functions - defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections - of the script should be parallelized over respective groups. - :param handlers: This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. - - - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. - - :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline - or a dict to store dialog :py:class:`~.Context`. - :param messenger_interface: An instance for this pipeline. - :param pre_services: List of :py:data:`~.ServiceBuilder` or - :py:data:`~.ServiceGroupBuilder` that will be executed before Actor. - :type pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] - :param post_services: List of :py:data:`~.ServiceBuilder` or - :py:data:`~.ServiceGroupBuilder` that will be executed after Actor. - It constructs root service group by merging `pre_services` + actor + `post_services`. - :type post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] - """ - pre_services = [] if pre_services is None else pre_services - post_services = [] if post_services is None else post_services - return cls( - pre_services=pre_services, - post_services=post_services, - script=script, - start_label=start_label, - fallback_label=fallback_label, - label_priority=label_priority, - condition_handler=condition_handler, - slots=slots, - parallelize_processing=parallelize_processing, - handlers=handlers, - messenger_interface=messenger_interface, - context_storage=context_storage, - ) - def set_actor( self, script: Union[Script, Dict], From 40484f0a077193c0bcb4771af90ea69be32a3bb6 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 18 Jul 2024 00:00:12 +0500 Subject: [PATCH 11/86] a bunch of fixes to type annotations + actor's call signature fixed --- chatsky/pipeline/pipeline/actor.py | 13 ++- chatsky/pipeline/pipeline/component.py | 6 -- chatsky/pipeline/pipeline/pipeline.py | 94 +++++++++++++++++++--- chatsky/pipeline/service/extra.py | 4 +- chatsky/pipeline/service/utils.py | 53 ------------ tests/pipeline/test_messenger_interface.py | 4 +- 6 files changed, 97 insertions(+), 77 deletions(-) delete mode 100644 chatsky/pipeline/service/utils.py diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index ad17d4f17..711704fee 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -92,9 +92,9 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) - handlers: Optional[Dict[ActorStage, List[Callable]]] = {} + handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default={}) _clean_turn_cache: Optional[bool] = True - # Making a 'computed field' for this feels overkill, a 'private' field is probably fine? + # Making a 'computed field' for this feels overkill, a 'private' field like this is probably fine? # Basically, Actor cannot be async with other components. That is correct, yes? @model_validator(mode="after") @@ -117,12 +117,19 @@ def actor_validator(self): if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: raise ValueError(f"Unknown fallback_label={self.fallback_label}") + # This line should be removed right after removing from_script() method. + self.handlers = {} if self.handlers is None else self.handlers + # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True - async def __call__(self, pipeline: Pipeline, ctx: Context): + # Standard signature of any PipelineComponent. ctx goes first. + async def __call__(self, ctx: Context, pipeline: Pipeline): await self.run_component(pipeline, ctx) + # This signature is mirroring to that of PipelineComponent. + # I think that should be changed, really. Not sure if that's important. + # Maybe some Actor tests will fail. async def run_component(self, pipeline: Pipeline, ctx: Context) -> None: """ Method for running an `Actor`. diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index a66823064..658e4128a 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -139,12 +139,6 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin except asyncio.TimeoutError: logger.warning(f"{type(self).__name__} '{self.name}' {extra_handler.stage} extra handler timed out!") - # Named this run_component, because ServiceGroup and Actor are components now too, and naming this run_service - # wouldn't be on point, they're not just services. The only problem I have is that this is kind of too generic, - # even confusingly generic, since _run() exists. Possible solution: implement _run within these classes - # themselves. My problem: centralizing Extra Handlers within PipelineComponent feels right. Why should Services - # have the right to run Extra Handlers however they want? They were already run there without checking the - # start_condition, which was a mistake. @abc.abstractmethod async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: raise NotImplementedError diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 6aa347c27..1b178e24f 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -93,19 +93,20 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): """ - # I wonder what happens/should happen here if only one callable is passed. - pre_services: Optional[List[Union[Service, ServiceGroup]]] = [] - post_services: Optional[List[Union[Service, ServiceGroup]]] = [] + # I wonder what happens/should happen here if just one callable is passed. + pre_services: List[Union[Service, ServiceGroup]] = Field(default=[]) + post_services: List[Union[Service, ServiceGroup]] = Field(default=[]) script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 - condition_handler: Optional[Callable] = None - handlers: Optional[Dict[ActorStage, List[Callable]]] = None + condition_handler: Callable = Field(default=default_condition_handler) + slots: Optional[Union[GroupSlot, Dict]] = None + handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default={}) messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None - before_handler: Optional[List[ExtraHandlerFunction]] = [] - after_handler: Optional[List[ExtraHandlerFunction]] = [] + before_handler: List[ExtraHandlerFunction] = Field(default=[]) + after_handler: List[ExtraHandlerFunction] = Field(default=[]) timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False @@ -113,7 +114,7 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): _services_pipeline: Optional[ServiceGroup] _clean_turn_cache: Optional[bool] - @computed_field(alias="_services_pipeline", repr=False) + @computed_field(repr=False) def _services_pipeline(self) -> ServiceGroup: components = [*self.pre_services, self.actor, *self.post_services] services_pipeline = ServiceGroup( @@ -175,9 +176,13 @@ def pipeline_init(self): # Same goes for @cached_property. Would @property work? self.actor = self._set_actor""" - # finalize_service_group() needs to have the search for Actor removed. - # Though this should work too. + # These lines should be removed right after removing the from_script() method. + self.context_storage = {} if self.context_storage is None else self.context_storage + # Same here, but this line creates a Pydantic error now, though it shouldn't have. + self.slots = GroupSlot.model_validate(self.slots) if self.slots is not None else None + finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) + # This could be removed. if self.actor is None: raise Exception("Actor wasn't initialized correctly!") @@ -245,13 +250,80 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } + # I know this function will be removed, but for testing I'll keep it for now + @classmethod + def from_script( + cls, + script: Union[Script, Dict], + start_label: NodeLabel2Type, + fallback_label: Optional[NodeLabel2Type] = None, + label_priority: float = 1.0, + condition_handler: Callable = default_condition_handler, + slots: Optional[Union[GroupSlot, Dict]] = None, + parallelize_processing: bool = False, + handlers: Optional[Dict[ActorStage, List[Callable]]] = None, + context_storage: Optional[Union[DBContextStorage, Dict]] = None, + messenger_interface: Optional[MessengerInterface] = CLIMessengerInterface(), + pre_services: Optional[List[Union[Service, ServiceGroup]]] = None, + post_services: Optional[List[Union[Service, ServiceGroup]]] = None, + ) -> "Pipeline": + """ + Pipeline script-based constructor. + It creates :py:class:`~.Actor` object and wraps it with pipeline. + NB! It is generally not designed for projects with complex structure. + :py:class:`~.Service` and :py:class:`~.ServiceGroup` customization + becomes not as obvious as it could be with it. + Should be preferred for simple workflows with Actor auto-execution. + :param script: (required) A :py:class:`~.Script` instance (object or dict). + :param start_label: (required) Actor start label. + :param fallback_label: Actor fallback label. + :param label_priority: Default priority value for all actor :py:const:`labels ` + where there is no priority. Defaults to `1.0`. + :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. + :param slots: Slots configuration. + :param parallelize_processing: This flag determines whether or not the functions + defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections + of the script should be parallelized over respective groups. + :param handlers: This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~chatsky.script.Actor`. + - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. + - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline + or a dict to store dialog :py:class:`~.Context`. + :param messenger_interface: An instance for this pipeline. + :param pre_services: List of :py:data:`~.ServiceBuilder` or + :py:data:`~.ServiceGroupBuilder` that will be executed before Actor. + :type pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] + :param post_services: List of :py:data:`~.ServiceBuilder` or + :py:data:`~.ServiceGroupBuilder` that will be executed after Actor. + It constructs root service group by merging `pre_services` + actor + `post_services`. + :type post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] + """ + pre_services = [] if pre_services is None else pre_services + post_services = [] if post_services is None else post_services + return cls( + pre_services=pre_services, + post_services=post_services, + script=script, + start_label=start_label, + fallback_label=fallback_label, + label_priority=label_priority, + condition_handler=condition_handler, + slots=slots, + parallelize_processing=parallelize_processing, + handlers=handlers, + messenger_interface=messenger_interface, + context_storage=context_storage, + components=[*pre_services, ACTOR, *post_services], + ) + def set_actor( self, script: Union[Script, Dict], start_label: NodeLabel2Type, fallback_label: Optional[NodeLabel2Type] = None, label_priority: float = 1.0, - condition_handler: Optional[Callable] = None, + condition_handler: Callable = default_condition_handler, handlers: Optional[Dict[ActorStage, List[Callable]]] = None, ): """ diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index ae0b0fc9c..a7f5d1395 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -51,8 +51,8 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T timeout: Optional[float] = None requested_async_flag: Optional[bool] = None - @computed_field(alias="calculated_async_flag", repr=False) - def calculate_async_flag(self) -> bool: + @computed_field(repr=False) + def calculated_async_flag(self) -> bool: return all([asyncio.iscoroutinefunction(func) for func in self.functions]) @property diff --git a/chatsky/pipeline/service/utils.py b/chatsky/pipeline/service/utils.py deleted file mode 100644 index 651f89b92..000000000 --- a/chatsky/pipeline/service/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Utility Functions ------------------ -The Utility Functions module contains several utility functions that are commonly used throughout Chatsky. -These functions provide a variety of utility functionality. -""" - -from typing import Any, Optional, Tuple, Mapping - - -def _get_attrs_with_updates( - obj: object, - drop_attrs: Optional[Tuple[str, ...]] = None, - replace_attrs: Optional[Mapping[str, str]] = None, - add_attrs: Optional[Mapping[str, Any]] = None, -) -> dict: - """ - Advanced customizable version of built-in `__dict__` property. - Sometimes during Pipeline construction `Services` (or `ServiceGroups`) should be rebuilt, - e.g. in case of some fields overriding. - This method can be customized to return a dict, - that can be spread (** operator) and passed to Service or ServiceGroup constructor. - Base dict is formed via `vars` built-in function. All "private" or "dunder" fields are omitted. - - :param drop_attrs: A tuple of key names that should be removed from the resulting dict. - :param replace_attrs: A mapping that should be replaced in the resulting dict. - :param add_attrs: A mapping that should be added to the resulting dict. - :return: Resulting dict. - """ - drop_attrs = () if drop_attrs is None else drop_attrs - replace_attrs = {} if replace_attrs is None else dict(replace_attrs) - add_attrs = {} if add_attrs is None else dict(add_attrs) - result = {} - for attribute in vars(obj): - if not attribute.startswith("__") and attribute not in drop_attrs: - if attribute in replace_attrs: - result[replace_attrs[attribute]] = getattr(obj, attribute) - else: - result[attribute] = getattr(obj, attribute) - result.update(add_attrs) - return result - - -def collect_defined_constructor_parameters_to_dict(**kwargs: Any): - """ - Function, that creates dict from non-`None` constructor parameters of pipeline component. - It is used in overriding component parameters, - when service handler or service group service is instance of Service or ServiceGroup (or dict). - It accepts same named parameters as component constructor. - - :return: Dict, containing key-value pairs of these parameters, that are not `None`. - """ - return dict([(key, value) for key, value in kwargs.items() if value is not None]) diff --git a/tests/pipeline/test_messenger_interface.py b/tests/pipeline/test_messenger_interface.py index b167a4cc4..545d41c2f 100644 --- a/tests/pipeline/test_messenger_interface.py +++ b/tests/pipeline/test_messenger_interface.py @@ -31,8 +31,8 @@ } } -pipeline = Pipeline.from_script( - SCRIPT, +pipeline = Pipeline( + script=SCRIPT, start_label=("pingpong_flow", "start_node"), fallback_label=("pingpong_flow", "fallback_node"), ) From 5df72bd62f1d63f868211b329c3d5a498c780c5a Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 18 Jul 2024 00:10:28 +0500 Subject: [PATCH 12/86] removed method call() from Actor so that it defaults to that of PipelineComponent --- chatsky/pipeline/pipeline/actor.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 711704fee..8795e412a 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -124,13 +124,7 @@ def actor_validator(self): self._clean_turn_cache = True # Standard signature of any PipelineComponent. ctx goes first. - async def __call__(self, ctx: Context, pipeline: Pipeline): - await self.run_component(pipeline, ctx) - - # This signature is mirroring to that of PipelineComponent. - # I think that should be changed, really. Not sure if that's important. - # Maybe some Actor tests will fail. - async def run_component(self, pipeline: Pipeline, ctx: Context) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ Method for running an `Actor`. From 0612023c1e53d851373e4bb26dc21d5b6cb242f8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 18 Jul 2024 19:36:40 +0500 Subject: [PATCH 13/86] fixed validation for ServiceGroups, Services and ExtraHandlers at Pipeline --- chatsky/pipeline/pipeline/pipeline.py | 48 +++++---------------------- chatsky/pipeline/service/extra.py | 16 +++++++-- chatsky/pipeline/service/group.py | 32 ++++++++++++------ chatsky/pipeline/service/service.py | 12 ++++++- 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 1b178e24f..62a1e4042 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -29,7 +29,7 @@ from chatsky.slots.slots import GroupSlot from chatsky.pipeline.service.service import Service from chatsky.pipeline.service.group import ServiceGroup -from chatsky.pipeline.service.extra import BeforeHandler, AfterHandler +from chatsky.pipeline.service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from ..types import ( ServiceFunction, GlobalExtraHandlerType, @@ -93,9 +93,9 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): """ - # I wonder what happens/should happen here if just one callable is passed. - pre_services: List[Union[Service, ServiceGroup]] = Field(default=[]) - post_services: List[Union[Service, ServiceGroup]] = Field(default=[]) + # Note to self: some testing required to see if [] works as intended. + pre_services: ServiceGroup = Field(default=[]) + post_services: ServiceGroup = Field(default=[]) script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None @@ -105,8 +105,8 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default={}) messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None - before_handler: List[ExtraHandlerFunction] = Field(default=[]) - after_handler: List[ExtraHandlerFunction] = Field(default=[]) + before_handler: ComponentExtraHandler = Field(default=[]) + after_handler: ComponentExtraHandler = Field(default=[]) timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False @@ -119,8 +119,8 @@ def _services_pipeline(self) -> ServiceGroup: components = [*self.pre_services, self.actor, *self.post_services] services_pipeline = ServiceGroup( components=components, - before_handler=BeforeHandler(self.before_handler), - after_handler=AfterHandler(self.after_handler), + before_handler=self.before_handler, + after_handler=self.after_handler, timeout=self.timeout, ) services_pipeline.name = "pipeline" @@ -138,37 +138,6 @@ def actor(self) -> Actor: handlers=self.handlers, ) - @field_validator("before_handler") - @classmethod - def single_before_handler_init(cls, handler: Any): - if isinstance(handler, ExtraHandlerFunction): - return [handler] - return handler - - @field_validator("after_handler") - @classmethod - def single_after_handler_init(cls, handler: Any): - if isinstance(handler, ExtraHandlerFunction): - return [handler] - return handler - - # This looks kind of terrible. I could remove this and ask the user to do things the right way, - # but this just seems more convenient for the user. Like, "put just one callable in pre-services"? Done. - # TODO: Change this to a large model_validator(mode="before") for less code bloat - @field_validator("pre_services") - @classmethod - def single_pre_service_init(cls, services: Any): - if not isinstance(services, List): - return [services] - return services - - @field_validator("post_services") - @classmethod - def single_post_service_init(cls, services: Any): - if not isinstance(services, List): - return [services] - return services - @model_validator(mode="after") def pipeline_init(self): """# I wonder if I could make actor itself a @computed_field, but I'm not sure that would work. @@ -314,7 +283,6 @@ def from_script( handlers=handlers, messenger_interface=messenger_interface, context_storage=context_storage, - components=[*pre_services, ACTOR, *post_services], ) def set_actor( diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index a7f5d1395..22fe444cd 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,8 +10,8 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING -from pydantic import BaseModel, computed_field +from typing import Optional, List, TYPE_CHECKING, Any +from pydantic import BaseModel, computed_field, model_validator from chatsky.script import Context @@ -51,6 +51,18 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T timeout: Optional[float] = None requested_async_flag: Optional[bool] = None + @model_validator(mode="before") + @classmethod + # Here Script class has "@validate_call". Is it needed here? + def functions_constructor(cls, data: Any): + result = data.copy() + if not isinstance(data, dict): + result = {"functions": data} + # Now it's definitely a dictionary. + if ("functions" in result) and (not isinstance(result["functions"], list)): + result["functions"] = [result["functions"]] + return result + @computed_field(repr=False) def calculated_async_flag(self) -> bool: return all([asyncio.iscoroutinefunction(func) for func in self.functions]) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index cf0ead616..7656d50ba 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio import logging -from typing import Optional, List, Union, Awaitable, TYPE_CHECKING, Any +from typing import Optional, List, Union, Awaitable, TYPE_CHECKING, Any, Callable, Dict from pydantic import model_validator, field_validator from chatsky.script import Context @@ -59,19 +59,29 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr group is executed only if it returns `True`. :param name: Requested group name. """ - - components: List[PipelineComponent] - - # Note to self: Here Script class has "@validate_call". Is it needed here? - @field_validator("components") + # If this is a list of PipelineComponents, why would the program know this is supposed to be a Service in the end? + # It's kind of logical it would try to match the best one fitting, but there are no guarantees, right? + # components: List[PipelineComponent] + components: List[Union[Service, ServiceGroup, Actor]] + + # Whenever data isn't passed like a dictionary, this tries to cast it to the right dictionary + # This includes List, PipelineComponent and Callable. + @model_validator(mode="before") @classmethod - def single_component_init(cls, comp: Any): - if isinstance(comp, PipelineComponent): - return [comp] - return comp + # Here Script class has "@validate_call". Is it needed here? + def components_constructor(cls, data: Any): + # Question: I don't think shallow copy() could be a problem for this, right? + # Pydantic is already rather recursively checking types. + result = data.copy() + if not isinstance(data, dict): + result = {"components": data} + # When it's a dictionary, data is cast to a list. + # We don't need to check if it's a list of Services or anything else: Pydantic does that for us. + if ("components" in result) and (not isinstance(result["components"], list)): + result["components"] = [result["components"]] + return result # Is there a better way to do this? calculated_async_flag is exposed to the user right now. - # Of course, they might not want to break their own program, but what if. # Maybe I could just make this a 'private' field, like '_calc_async' @model_validator(mode="after") def calculate_async_flag(self): diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index fd67fe9f8..835e5c3cd 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,7 +13,7 @@ from __future__ import annotations import logging import inspect -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Any from pydantic import model_validator from chatsky.script import Context @@ -57,6 +57,16 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): handler: ServiceFunction + # This code handles cases where just one Callable is passed into it's constructor data. + # All flags will be on default in that case. + @model_validator(mode="before") + @classmethod + # Here Script class has "@validate_call". Is it needed here? + def handler_constructor(cls, data: Any): + if not isinstance(data, dict): + return {"handler": data} + return data + @model_validator(mode="after") def tick_async_flag(self): self.calculated_async_flag = True From 1e67936d7518bbf99c4254cf4dcd92a175c38b88 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 18 Jul 2024 21:40:17 +0500 Subject: [PATCH 14/86] an error fixed, pipeline declaration conflicts found (they sent tuples to from_script() before, that's not handled right now) --- chatsky/pipeline/pipeline/actor.py | 3 ++- chatsky/pipeline/pipeline/pipeline.py | 10 +++++----- chatsky/pipeline/service/group.py | 9 +++++---- chatsky/pipeline/service/service.py | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 8795e412a..60e86b249 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -92,7 +92,7 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) - handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default={}) + handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default_factory=dict) _clean_turn_cache: Optional[bool] = True # Making a 'computed field' for this feels overkill, a 'private' field like this is probably fine? @@ -122,6 +122,7 @@ def actor_validator(self): # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True + return self # Standard signature of any PipelineComponent. ctx goes first. async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 62a1e4042..31ea401ba 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -94,19 +94,19 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): """ # Note to self: some testing required to see if [] works as intended. - pre_services: ServiceGroup = Field(default=[]) - post_services: ServiceGroup = Field(default=[]) + pre_services: ServiceGroup = Field(default_factory=list) + post_services: ServiceGroup = Field(default_factory=list) script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) slots: Optional[Union[GroupSlot, Dict]] = None - handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default={}) + handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default_factory=dict) messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Optional[Union[DBContextStorage, Dict]] = None - before_handler: ComponentExtraHandler = Field(default=[]) - after_handler: ComponentExtraHandler = Field(default=[]) + before_handler: ComponentExtraHandler = Field(default_factory=list) + after_handler: ComponentExtraHandler = Field(default_factory=list) timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 7656d50ba..69218ea6b 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -62,7 +62,7 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr # If this is a list of PipelineComponents, why would the program know this is supposed to be a Service in the end? # It's kind of logical it would try to match the best one fitting, but there are no guarantees, right? # components: List[PipelineComponent] - components: List[Union[Service, ServiceGroup, Actor]] + components: List[Union[Actor, Service, ServiceGroup,]] # Whenever data isn't passed like a dictionary, this tries to cast it to the right dictionary # This includes List, PipelineComponent and Callable. @@ -72,13 +72,14 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr def components_constructor(cls, data: Any): # Question: I don't think shallow copy() could be a problem for this, right? # Pydantic is already rather recursively checking types. + # print(data) result = data.copy() if not isinstance(data, dict): - result = {"components": data} + result = {'components': data} # When it's a dictionary, data is cast to a list. # We don't need to check if it's a list of Services or anything else: Pydantic does that for us. - if ("components" in result) and (not isinstance(result["components"], list)): - result["components"] = [result["components"]] + if ('components' in result) and (not isinstance(result['components'], list)): + result['components'] = [result['components']] return result # Is there a better way to do this? calculated_async_flag is exposed to the user right now. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 835e5c3cd..c8495505f 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -63,7 +63,7 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): @classmethod # Here Script class has "@validate_call". Is it needed here? def handler_constructor(cls, data: Any): - if not isinstance(data, dict): + if not isinstance(data, dict) and not isinstance(data, list): return {"handler": data} return data From 049f8f34647f28876947e5100af36e21277a1b68 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 12:11:14 +0500 Subject: [PATCH 15/86] replaced from_script() with Pipeline() throughout the codebase, replaced TOY_SCRIPT_ARGS with TOY_SCRIPT_KWARGS --- chatsky/utils/testing/__init__.py | 2 +- chatsky/utils/testing/toy_script.py | 8 +++++--- tests/context_storages/test_dbs.py | 4 ++-- tests/pipeline/test_parallel_processing.py | 4 ++-- tests/pipeline/test_pipeline.py | 2 +- tests/pipeline/test_update_ctx_misc.py | 2 +- tests/script/conditions/test_conditions.py | 2 +- tests/script/core/test_actor.py | 20 +++++++++---------- tests/script/core/test_normalization.py | 2 +- tests/script/labels/test_labels.py | 4 ++-- tests/script/responses/test_responses.py | 2 +- tests/slots/conftest.py | 2 +- tests/stats/test_defaults.py | 2 +- tutorials/context_storages/1_basics.py | 4 ++-- tutorials/context_storages/2_postgresql.py | 4 ++-- tutorials/context_storages/3_mongodb.py | 4 ++-- tutorials/context_storages/4_redis.py | 4 ++-- tutorials/context_storages/5_mysql.py | 4 ++-- tutorials/context_storages/6_sqlite.py | 4 ++-- .../context_storages/7_yandex_database.py | 4 ++-- tutorials/messengers/telegram/1_basic.py | 2 +- .../messengers/telegram/2_attachments.py | 2 +- tutorials/messengers/telegram/3_advanced.py | 2 +- .../messengers/web_api_interface/1_fastapi.py | 6 +++--- .../web_api_interface/2_websocket_chat.py | 6 +++--- tutorials/pipeline/1_basics.py | 18 ++++++++--------- .../pipeline/2_pre_and_post_processors.py | 6 +++--- tutorials/script/core/1_basics.py | 6 +++--- tutorials/script/core/2_conditions.py | 4 ++-- tutorials/script/core/3_responses.py | 4 ++-- tutorials/script/core/4_transitions.py | 4 ++-- tutorials/script/core/5_global_transitions.py | 4 ++-- .../script/core/6_context_serialization.py | 4 ++-- .../script/core/7_pre_response_processing.py | 4 ++-- tutorials/script/core/8_misc.py | 4 ++-- .../core/9_pre_transitions_processing.py | 4 ++-- tutorials/script/responses/1_basics.py | 4 ++-- tutorials/script/responses/2_media.py | 4 ++-- tutorials/script/responses/3_multi_message.py | 4 ++-- tutorials/slots/1_basic_example.py | 4 ++-- tutorials/utils/1_cache.py | 2 +- tutorials/utils/2_lru_cache.py | 2 +- 42 files changed, 93 insertions(+), 91 deletions(-) diff --git a/chatsky/utils/testing/__init__.py b/chatsky/utils/testing/__init__.py index 2e13da083..2081e6dd4 100644 --- a/chatsky/utils/testing/__init__.py +++ b/chatsky/utils/testing/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from .common import is_interactive_mode, check_happy_path, run_interactive_mode -from .toy_script import TOY_SCRIPT, TOY_SCRIPT_ARGS, HAPPY_PATH +from .toy_script import TOY_SCRIPT, TOY_SCRIPT_KWARGS, HAPPY_PATH from .response_comparers import default_comparer try: diff --git a/chatsky/utils/testing/toy_script.py b/chatsky/utils/testing/toy_script.py index 1f0c38dd4..a898298eb 100644 --- a/chatsky/utils/testing/toy_script.py +++ b/chatsky/utils/testing/toy_script.py @@ -39,14 +39,16 @@ :meta hide-value: """ -TOY_SCRIPT_ARGS = (TOY_SCRIPT, ("greeting_flow", "start_node"), ("greeting_flow", "fallback_node")) +TOY_SCRIPT_KWARGS = {"script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node")} """ -Arguments to pass to :py:meth:`~chatsky.pipeline.pipeline.pipeline.Pipeline.from_script` in order to +# There should be a better description of this +Keyword arguments to pass to :py:meth:`~chatsky.pipeline.pipeline.pipeline.Pipeline` in order to use :py:data:`~.TOY_SCRIPT`: .. code-block:: - Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=..., ...) + Pipeline(**TOY_SCRIPT_KWARGS, context_storage=...) :meta hide-value: """ diff --git a/tests/context_storages/test_dbs.py b/tests/context_storages/test_dbs.py index 0d37d2a0d..43820e68f 100644 --- a/tests/context_storages/test_dbs.py +++ b/tests/context_storages/test_dbs.py @@ -33,7 +33,7 @@ from tests.test_utils import get_path_from_tests_to_current_dir from chatsky.pipeline import Pipeline -from chatsky.utils.testing import check_happy_path, TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing import check_happy_path, TOY_SCRIPT_KWARGS, HAPPY_PATH dot_path_to_addon = get_path_from_tests_to_current_dir(__file__, separator=".") @@ -84,7 +84,7 @@ def generic_test(db, testing_context, context_id): assert context_id not in db # test `get` method assert db.get(context_id) is None - pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) check_happy_path(pipeline, happy_path=HAPPY_PATH) diff --git a/tests/pipeline/test_parallel_processing.py b/tests/pipeline/test_parallel_processing.py index b243c5f4b..4c23e344a 100644 --- a/tests/pipeline/test_parallel_processing.py +++ b/tests/pipeline/test_parallel_processing.py @@ -29,14 +29,14 @@ async def slow_processing(ctx, _): } # test sequential processing - pipeline = Pipeline.from_script(toy_script, start_label=("root", "start"), parallelize_processing=False) + pipeline = Pipeline(script=toy_script, start_label=("root", "start"), parallelize_processing=False) ctx = await pipeline._run_pipeline(Message(), 0) assert ctx.last_response.text == "fast: slow: text" # test parallel processing - pipeline = Pipeline.from_script(toy_script, start_label=("root", "start"), parallelize_processing=True) + pipeline = Pipeline(script=toy_script, start_label=("root", "start"), parallelize_processing=True) ctx = await pipeline._run_pipeline(Message(), 0) diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index cb8158984..96fba494a 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -17,7 +17,7 @@ def test_pretty_format(): def test_script_getting_and_setting(): script = {"old_flow": {"": {RESPONSE: lambda _, __: Message(), TRANSITIONS: {"": cnd.true()}}}} - pipeline = Pipeline.from_script(script=script, start_label=("old_flow", "")) + pipeline = Pipeline(script=script, start_label=("old_flow", "")) new_script = {"new_flow": {"": {RESPONSE: lambda _, __: Message(), TRANSITIONS: {"": cnd.false()}}}} pipeline.set_actor(script=new_script, start_label=("new_flow", "")) diff --git a/tests/pipeline/test_update_ctx_misc.py b/tests/pipeline/test_update_ctx_misc.py index 5a5a5d4c5..4fc2fddb8 100644 --- a/tests/pipeline/test_update_ctx_misc.py +++ b/tests/pipeline/test_update_ctx_misc.py @@ -19,7 +19,7 @@ def condition(ctx, _): } } - pipeline = Pipeline.from_script(toy_script, ("root", "start"), ("root", "failure")) + pipeline = Pipeline(script=toy_script, start_label=("root", "start"), fallback_label=("root", "failure")) ctx = await pipeline._run_pipeline(Message(), 0, update_ctx_misc={"condition": True}) diff --git a/tests/script/conditions/test_conditions.py b/tests/script/conditions/test_conditions.py index 674ed57d0..f8ae26103 100644 --- a/tests/script/conditions/test_conditions.py +++ b/tests/script/conditions/test_conditions.py @@ -12,7 +12,7 @@ def test_conditions(): failed_ctx = Context() failed_ctx.add_request(Message()) failed_ctx.add_label(label) - pipeline = Pipeline.from_script(script={"flow": {"node": {}}}, start_label=("flow", "node")) + pipeline = Pipeline(script={"flow": {"node": {}}}, start_label=("flow", "node")) assert cnd.exact_match("text")(ctx, pipeline) assert cnd.exact_match(Message("text", misc={}))(ctx, pipeline) diff --git a/tests/script/core/test_actor.py b/tests/script/core/test_actor.py index c2ee2b69b..4c6b2722b 100644 --- a/tests/script/core/test_actor.py +++ b/tests/script/core/test_actor.py @@ -51,26 +51,26 @@ def raised_response(ctx: Context, pipeline): async def test_actor(): try: # fail of start label - Pipeline.from_script({"flow": {"node1": {}}}, start_label=("flow1", "node1")) + Pipeline(script={"flow": {"node1": {}}}, start_label=("flow1", "node1")) raise Exception("can not be passed: fail of start label") except ValueError: pass try: # fail of fallback label - Pipeline.from_script({"flow": {"node1": {}}}, start_label=("flow", "node1"), fallback_label=("flow1", "node1")) + Pipeline(script={"flow": {"node1": {}}}, start_label=("flow", "node1"), fallback_label=("flow1", "node1")) raise Exception("can not be passed: fail of fallback label") except ValueError: pass try: # fail of missing node - Pipeline.from_script({"flow": {"node1": {TRANSITIONS: {"miss_node1": true()}}}}, start_label=("flow", "node1")) + Pipeline(script={"flow": {"node1": {TRANSITIONS: {"miss_node1": true()}}}}, start_label=("flow", "node1")) raise Exception("can not be passed: fail of missing node") except ValueError: pass try: # fail of response returned Callable - pipeline = Pipeline.from_script( - {"flow": {"node1": {RESPONSE: lambda c, a: lambda x: 1, TRANSITIONS: {repeat(): true()}}}}, + pipeline = Pipeline( + script={"flow": {"node1": {RESPONSE: lambda c, a: lambda x: 1, TRANSITIONS: {repeat(): true()}}}}, start_label=("flow", "node1"), ) ctx = Context() @@ -80,15 +80,15 @@ async def test_actor(): pass # empty ctx stability - pipeline = Pipeline.from_script( - {"flow": {"node1": {TRANSITIONS: {"node1": true()}}}}, start_label=("flow", "node1") + pipeline = Pipeline( + script={"flow": {"node1": {TRANSITIONS: {"node1": true()}}}}, start_label=("flow", "node1") ) ctx = Context() await pipeline.actor(pipeline, ctx) # fake label stability - pipeline = Pipeline.from_script( - {"flow": {"node1": {TRANSITIONS: {fake_label: true()}}}}, start_label=("flow", "node1") + pipeline = Pipeline( + script={"flow": {"node1": {TRANSITIONS: {fake_label: true()}}}}, start_label=("flow", "node1") ) ctx = Context() await pipeline.actor(pipeline, ctx) @@ -195,7 +195,7 @@ async def test_call_limit(): }, } # script = {"flow": {"node1": {TRANSITIONS: {"node1": true()}}}} - pipeline = Pipeline.from_script(script=script, start_label=("flow1", "node1")) + pipeline = Pipeline(script=script, start_label=("flow1", "node1")) for i in range(4): await pipeline._run_pipeline(Message("req1"), 0) if limit_errors: diff --git a/tests/script/core/test_normalization.py b/tests/script/core/test_normalization.py index cda6b6b36..347486949 100644 --- a/tests/script/core/test_normalization.py +++ b/tests/script/core/test_normalization.py @@ -28,7 +28,7 @@ def std_func(ctx, pipeline): def create_env() -> Tuple[Context, Pipeline]: ctx = Context() script = {"flow": {"node1": {TRANSITIONS: {repeat(): true()}, RESPONSE: Message("response")}}} - pipeline = Pipeline.from_script(script=script, start_label=("flow", "node1"), fallback_label=("flow", "node1")) + pipeline = Pipeline(script=script, start_label=("flow", "node1"), fallback_label=("flow", "node1")) ctx.add_request(Message("text")) return ctx, pipeline diff --git a/tests/script/labels/test_labels.py b/tests/script/labels/test_labels.py index a03937999..fcb109adb 100644 --- a/tests/script/labels/test_labels.py +++ b/tests/script/labels/test_labels.py @@ -6,7 +6,7 @@ def test_labels(): ctx = Context() - pipeline = Pipeline.from_script( + pipeline = Pipeline( script={"flow": {"node1": {}, "node2": {}, "node3": {}}, "service": {"start": {}, "fallback": {}}}, start_label=("service", "start"), fallback_label=("service", "fallback"), @@ -36,7 +36,7 @@ def test_labels(): assert backward(99, cyclicality_flag=False)(ctx, pipeline) == ("service", "fallback", 99) ctx = Context() ctx.add_label(["flow", "node2"]) - pipeline = Pipeline.from_script( + pipeline = Pipeline( script={"flow": {"node1": {}}, "service": {"start": {}, "fallback": {}}}, start_label=("service", "start"), fallback_label=("service", "fallback"), diff --git a/tests/script/responses/test_responses.py b/tests/script/responses/test_responses.py index 53157f610..c281a9b31 100644 --- a/tests/script/responses/test_responses.py +++ b/tests/script/responses/test_responses.py @@ -6,6 +6,6 @@ def test_response(): ctx = Context() - pipeline = Pipeline.from_script(script={"flow": {"node": {}}}, start_label=("flow", "node")) + pipeline = Pipeline(script={"flow": {"node": {}}}, start_label=("flow", "node")) for _ in range(10): assert choice(["text1", "text2"])(ctx, pipeline) in ["text1", "text2"] diff --git a/tests/slots/conftest.py b/tests/slots/conftest.py index 5d94cf63d..8539b013a 100644 --- a/tests/slots/conftest.py +++ b/tests/slots/conftest.py @@ -17,7 +17,7 @@ def patch_exception_equality(monkeypatch): @pytest.fixture(scope="function") def pipeline(): script = {"flow": {"node": {RESPONSE: Message(), TRANSITIONS: {"node": cnd.true()}}}} - pipeline = Pipeline.from_script(script=script, start_label=("flow", "node")) + pipeline = Pipeline(script=script, start_label=("flow", "node")) return pipeline diff --git a/tests/stats/test_defaults.py b/tests/stats/test_defaults.py index 43f3d4fe5..a770fdc5a 100644 --- a/tests/stats/test_defaults.py +++ b/tests/stats/test_defaults.py @@ -21,7 +21,7 @@ ], ) async def test_get_current_label(context: Context, expected: set): - pipeline = Pipeline.from_script({"greeting_flow": {"start_node": {}}}, ("greeting_flow", "start_node")) + pipeline = Pipeline(script={"greeting_flow": {"start_node": {}}}, start_label=("greeting_flow", "start_node")) runtime_info = ExtraHandlerRuntimeInfo( func=lambda x: x, stage="BEFORE", diff --git a/tutorials/context_storages/1_basics.py b/tutorials/context_storages/1_basics.py index ce5d484a5..4ea9c9d51 100644 --- a/tutorials/context_storages/1_basics.py +++ b/tutorials/context_storages/1_basics.py @@ -23,14 +23,14 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH pathlib.Path("dbs").mkdir(exist_ok=True) db = context_storage_factory("json://dbs/file.json") # db = context_storage_factory("pickle://dbs/file.pkl") # db = context_storage_factory("shelve://dbs/file") -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/context_storages/2_postgresql.py b/tutorials/context_storages/2_postgresql.py index 8ca07e95b..4124c4799 100644 --- a/tutorials/context_storages/2_postgresql.py +++ b/tutorials/context_storages/2_postgresql.py @@ -25,7 +25,7 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -37,7 +37,7 @@ db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/context_storages/3_mongodb.py b/tutorials/context_storages/3_mongodb.py index a68512ab4..0b2919e73 100644 --- a/tutorials/context_storages/3_mongodb.py +++ b/tutorials/context_storages/3_mongodb.py @@ -24,7 +24,7 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -35,7 +35,7 @@ ) db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/context_storages/4_redis.py b/tutorials/context_storages/4_redis.py index 51dfee008..5fe215ddf 100644 --- a/tutorials/context_storages/4_redis.py +++ b/tutorials/context_storages/4_redis.py @@ -24,7 +24,7 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -34,7 +34,7 @@ db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/context_storages/5_mysql.py b/tutorials/context_storages/5_mysql.py index b52a5c3f6..b9f9d01a0 100644 --- a/tutorials/context_storages/5_mysql.py +++ b/tutorials/context_storages/5_mysql.py @@ -25,7 +25,7 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -37,7 +37,7 @@ db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/context_storages/6_sqlite.py b/tutorials/context_storages/6_sqlite.py index 76ede50e8..b9e8ea8a9 100644 --- a/tutorials/context_storages/6_sqlite.py +++ b/tutorials/context_storages/6_sqlite.py @@ -28,7 +28,7 @@ is_interactive_mode, run_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -41,7 +41,7 @@ db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/context_storages/7_yandex_database.py b/tutorials/context_storages/7_yandex_database.py index 294744cb4..be5ac7a69 100644 --- a/tutorials/context_storages/7_yandex_database.py +++ b/tutorials/context_storages/7_yandex_database.py @@ -24,7 +24,7 @@ run_interactive_mode, is_interactive_mode, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from chatsky.utils.testing.toy_script import TOY_SCRIPT_KWARGS, HAPPY_PATH # %% @@ -42,7 +42,7 @@ ) db = context_storage_factory(db_uri) -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) +pipeline = Pipeline(**TOY_SCRIPT_KWARGS, context_storage=db) # %% diff --git a/tutorials/messengers/telegram/1_basic.py b/tutorials/messengers/telegram/1_basic.py index cb050bb89..a7379a91d 100644 --- a/tutorials/messengers/telegram/1_basic.py +++ b/tutorials/messengers/telegram/1_basic.py @@ -70,7 +70,7 @@ class and [python-telegram-bot](https://docs.python-telegram-bot.org/) # %% -pipeline = Pipeline.from_script( +pipeline = Pipeline( script=script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), diff --git a/tutorials/messengers/telegram/2_attachments.py b/tutorials/messengers/telegram/2_attachments.py index 93c8d233d..461710169 100644 --- a/tutorials/messengers/telegram/2_attachments.py +++ b/tutorials/messengers/telegram/2_attachments.py @@ -258,7 +258,7 @@ class and [python-telegram-bot](https://docs.python-telegram-bot.org/) # %% -pipeline = Pipeline.from_script( +pipeline = Pipeline( script=script, start_label=("main_flow", "start_node"), fallback_label=("main_flow", "fallback_node"), diff --git a/tutorials/messengers/telegram/3_advanced.py b/tutorials/messengers/telegram/3_advanced.py index c10624db9..bed4da369 100644 --- a/tutorials/messengers/telegram/3_advanced.py +++ b/tutorials/messengers/telegram/3_advanced.py @@ -247,7 +247,7 @@ async def hash_data_attachment_request(ctx: Context, pipe: Pipeline) -> Message: # %% -pipeline = Pipeline.from_script( +pipeline = Pipeline( script=script, start_label=("main_flow", "start_node"), fallback_label=("main_flow", "fallback_node"), diff --git a/tutorials/messengers/web_api_interface/1_fastapi.py b/tutorials/messengers/web_api_interface/1_fastapi.py index a3fed68eb..ef07009ed 100644 --- a/tutorials/messengers/web_api_interface/1_fastapi.py +++ b/tutorials/messengers/web_api_interface/1_fastapi.py @@ -20,7 +20,7 @@ from chatsky.messengers.common.interface import CallbackMessengerInterface from chatsky.script import Message from chatsky.pipeline import Pipeline -from chatsky.utils.testing import TOY_SCRIPT_ARGS, is_interactive_mode +from chatsky.utils.testing import TOY_SCRIPT_KWARGS, is_interactive_mode import uvicorn from pydantic import BaseModel @@ -83,8 +83,8 @@ # %% messenger_interface = CallbackMessengerInterface() # CallbackMessengerInterface instantiating the dedicated messenger interface -pipeline = Pipeline.from_script( - *TOY_SCRIPT_ARGS, messenger_interface=messenger_interface +pipeline = Pipeline( + **TOY_SCRIPT_KWARGS, messenger_interface=messenger_interface ) diff --git a/tutorials/messengers/web_api_interface/2_websocket_chat.py b/tutorials/messengers/web_api_interface/2_websocket_chat.py index 7163899c8..af766d1f5 100644 --- a/tutorials/messengers/web_api_interface/2_websocket_chat.py +++ b/tutorials/messengers/web_api_interface/2_websocket_chat.py @@ -27,7 +27,7 @@ from chatsky.messengers.common.interface import CallbackMessengerInterface from chatsky.script import Message from chatsky.pipeline import Pipeline -from chatsky.utils.testing import TOY_SCRIPT_ARGS, is_interactive_mode +from chatsky.utils.testing import TOY_SCRIPT_KWARGS, is_interactive_mode import uvicorn from fastapi import FastAPI, WebSocket, WebSocketDisconnect @@ -36,8 +36,8 @@ # %% messenger_interface = CallbackMessengerInterface() -pipeline = Pipeline.from_script( - *TOY_SCRIPT_ARGS, messenger_interface=messenger_interface +pipeline = Pipeline( + **TOY_SCRIPT_KWARGS, messenger_interface=messenger_interface ) diff --git a/tutorials/pipeline/1_basics.py b/tutorials/pipeline/1_basics.py index fe285e15e..ad57bda83 100644 --- a/tutorials/pipeline/1_basics.py +++ b/tutorials/pipeline/1_basics.py @@ -22,7 +22,7 @@ is_interactive_mode, HAPPY_PATH, TOY_SCRIPT, - TOY_SCRIPT_ARGS, + TOY_SCRIPT_KWARGS, ) @@ -50,8 +50,8 @@ """ # %% -pipeline = Pipeline.from_script( - TOY_SCRIPT, +pipeline = Pipeline( + script=TOY_SCRIPT, # Pipeline script object, defined in `chatsky.utils.testing.toy_script` start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), @@ -61,15 +61,15 @@ # %% [markdown] """ For the sake of brevity, other tutorials -might use `TOY_SCRIPT_ARGS` to initialize pipeline: +might use `TOY_SCRIPT_KWARGS` to initialize pipeline: """ # %% -assert TOY_SCRIPT_ARGS == ( - TOY_SCRIPT, - ("greeting_flow", "start_node"), - ("greeting_flow", "fallback_node"), -) +assert TOY_SCRIPT_KWARGS == { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), +} # %% diff --git a/tutorials/pipeline/2_pre_and_post_processors.py b/tutorials/pipeline/2_pre_and_post_processors.py index 2f418d41a..8160a7ebb 100644 --- a/tutorials/pipeline/2_pre_and_post_processors.py +++ b/tutorials/pipeline/2_pre_and_post_processors.py @@ -23,7 +23,7 @@ check_happy_path, is_interactive_mode, HAPPY_PATH, - TOY_SCRIPT_ARGS, + TOY_SCRIPT_KWARGS, ) logger = logging.getLogger(__name__) @@ -65,8 +65,8 @@ def pong_processor(ctx: Context): # %% -pipeline = Pipeline.from_script( - *TOY_SCRIPT_ARGS, +pipeline = Pipeline( + **TOY_SCRIPT_KWARGS, context_storage={}, # `context_storage` - a dictionary or # a `DBContextStorage` instance, # a place to store dialog contexts diff --git a/tutorials/script/core/1_basics.py b/tutorials/script/core/1_basics.py index ecde97725..49b39c41b 100644 --- a/tutorials/script/core/1_basics.py +++ b/tutorials/script/core/1_basics.py @@ -6,7 +6,7 @@ Here, basic usege of %mddoclink(api,pipeline.pipeline.pipeline,Pipeline) primitive is shown: its' creation with -%mddoclink(api,pipeline.pipeline.pipeline,Pipeline.from_script) +%mddoclink(api,pipeline.pipeline.pipeline,Pipeline) and execution. Additionally, function %mddoclink(api,utils.testing.common,check_happy_path) @@ -139,8 +139,8 @@ # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), ) diff --git a/tutorials/script/core/2_conditions.py b/tutorials/script/core/2_conditions.py index afe52762b..b355f5908 100644 --- a/tutorials/script/core/2_conditions.py +++ b/tutorials/script/core/2_conditions.py @@ -212,8 +212,8 @@ def internal_condition_function(ctx: Context, _: Pipeline) -> bool: ) # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), ) diff --git a/tutorials/script/core/3_responses.py b/tutorials/script/core/3_responses.py index 25fa067be..fa05ced13 100644 --- a/tutorials/script/core/3_responses.py +++ b/tutorials/script/core/3_responses.py @@ -189,8 +189,8 @@ def fallback_trace_response(ctx: Context, _: Pipeline) -> Message: random.seed(31415) # predestination of choice -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), ) diff --git a/tutorials/script/core/4_transitions.py b/tutorials/script/core/4_transitions.py index 777e03652..0b7692498 100644 --- a/tutorials/script/core/4_transitions.py +++ b/tutorials/script/core/4_transitions.py @@ -295,8 +295,8 @@ def transition(_: Context, __: Pipeline) -> ConstLabel: ) # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("global_flow", "start_node"), fallback_label=("global_flow", "fallback_node"), ) diff --git a/tutorials/script/core/5_global_transitions.py b/tutorials/script/core/5_global_transitions.py index d6e3037a6..2b3c45005 100644 --- a/tutorials/script/core/5_global_transitions.py +++ b/tutorials/script/core/5_global_transitions.py @@ -196,8 +196,8 @@ ) # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("global_flow", "start_node"), fallback_label=("global_flow", "fallback_node"), ) diff --git a/tutorials/script/core/6_context_serialization.py b/tutorials/script/core/6_context_serialization.py index 759c715ef..fbbcc9bb1 100644 --- a/tutorials/script/core/6_context_serialization.py +++ b/tutorials/script/core/6_context_serialization.py @@ -77,8 +77,8 @@ def process_response(ctx: Context): # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("flow_start", "node_start"), post_services=[process_response], ) diff --git a/tutorials/script/core/7_pre_response_processing.py b/tutorials/script/core/7_pre_response_processing.py index 48a88f17a..0fe561d1e 100644 --- a/tutorials/script/core/7_pre_response_processing.py +++ b/tutorials/script/core/7_pre_response_processing.py @@ -117,8 +117,8 @@ def add_prefix_processing(ctx: Context, _: Pipeline): # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("root", "start"), fallback_label=("root", "fallback"), ) diff --git a/tutorials/script/core/8_misc.py b/tutorials/script/core/8_misc.py index 456fe232b..555f8eebc 100644 --- a/tutorials/script/core/8_misc.py +++ b/tutorials/script/core/8_misc.py @@ -142,8 +142,8 @@ def custom_response(ctx: Context, _: Pipeline) -> Message: # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("root", "start"), fallback_label=("root", "fallback"), ) diff --git a/tutorials/script/core/9_pre_transitions_processing.py b/tutorials/script/core/9_pre_transitions_processing.py index 3d5ff101c..e99fffdb3 100644 --- a/tutorials/script/core/9_pre_transitions_processing.py +++ b/tutorials/script/core/9_pre_transitions_processing.py @@ -87,8 +87,8 @@ def prepend_previous_node_response(ctx: Context, _: Pipeline): # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("root", "start"), fallback_label=("root", "fallback"), ) diff --git a/tutorials/script/responses/1_basics.py b/tutorials/script/responses/1_basics.py index feeb6e8ca..4202af96c 100644 --- a/tutorials/script/responses/1_basics.py +++ b/tutorials/script/responses/1_basics.py @@ -87,8 +87,8 @@ class CallbackRequest(NamedTuple): # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), ) diff --git a/tutorials/script/responses/2_media.py b/tutorials/script/responses/2_media.py index 16838d7dd..ed5f9c288 100644 --- a/tutorials/script/responses/2_media.py +++ b/tutorials/script/responses/2_media.py @@ -116,8 +116,8 @@ # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("root", "start"), fallback_label=("root", "fallback"), ) diff --git a/tutorials/script/responses/3_multi_message.py b/tutorials/script/responses/3_multi_message.py index d6504c260..1f24a7be8 100644 --- a/tutorials/script/responses/3_multi_message.py +++ b/tutorials/script/responses/3_multi_message.py @@ -144,8 +144,8 @@ # %% -pipeline = Pipeline.from_script( - toy_script, +pipeline = Pipeline( + script=toy_script, start_label=("greeting_flow", "start_node"), fallback_label=("greeting_flow", "fallback_node"), ) diff --git a/tutorials/slots/1_basic_example.py b/tutorials/slots/1_basic_example.py index f65320c20..1c528de52 100644 --- a/tutorials/slots/1_basic_example.py +++ b/tutorials/slots/1_basic_example.py @@ -217,8 +217,8 @@ ] # %% -pipeline = Pipeline.from_script( - script, +pipeline = Pipeline( + script=script, start_label=("root", "start"), fallback_label=("root", "fallback"), slots=SLOTS, diff --git a/tutorials/utils/1_cache.py b/tutorials/utils/1_cache.py index 1f1dd7ec9..5c3b6980b 100644 --- a/tutorials/utils/1_cache.py +++ b/tutorials/utils/1_cache.py @@ -65,7 +65,7 @@ def response(_: Context, __: Pipeline) -> Message: (Message(), "5-6-5-6"), ) -pipeline = Pipeline.from_script(toy_script, start_label=("flow", "node1")) +pipeline = Pipeline(script=toy_script, start_label=("flow", "node1")) # %% diff --git a/tutorials/utils/2_lru_cache.py b/tutorials/utils/2_lru_cache.py index 0af5d27f2..241a0a2e2 100644 --- a/tutorials/utils/2_lru_cache.py +++ b/tutorials/utils/2_lru_cache.py @@ -64,7 +64,7 @@ def response(_: Context, __: Pipeline) -> Message: (Message(), "9-10-11-10-12"), ) -pipeline = Pipeline.from_script(toy_script, start_label=("flow", "node1")) +pipeline = Pipeline(script=toy_script, start_label=("flow", "node1")) # %% if __name__ == "__main__": From c5413b868ff7fb6bb57bd06eb379b96c0093bfb3 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 12:42:03 +0500 Subject: [PATCH 16/86] minor changes and formatted with poetry --- chatsky/pipeline/service/group.py | 15 +++++++++++---- chatsky/utils/testing/toy_script.py | 7 +++++-- tests/pipeline/test_pipeline.py | 16 +++++----------- tests/script/core/test_actor.py | 14 +++++--------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 69218ea6b..3f6c356e5 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -59,10 +59,17 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr group is executed only if it returns `True`. :param name: Requested group name. """ + # If this is a list of PipelineComponents, why would the program know this is supposed to be a Service in the end? # It's kind of logical it would try to match the best one fitting, but there are no guarantees, right? # components: List[PipelineComponent] - components: List[Union[Actor, Service, ServiceGroup,]] + components: List[ + Union[ + Actor, + Service, + ServiceGroup, + ] + ] # Whenever data isn't passed like a dictionary, this tries to cast it to the right dictionary # This includes List, PipelineComponent and Callable. @@ -75,11 +82,11 @@ def components_constructor(cls, data: Any): # print(data) result = data.copy() if not isinstance(data, dict): - result = {'components': data} + result = {"components": data} # When it's a dictionary, data is cast to a list. # We don't need to check if it's a list of Services or anything else: Pydantic does that for us. - if ('components' in result) and (not isinstance(result['components'], list)): - result['components'] = [result['components']] + if ("components" in result) and (not isinstance(result["components"], list)): + result["components"] = [result["components"]] return result # Is there a better way to do this? calculated_async_flag is exposed to the user right now. diff --git a/chatsky/utils/testing/toy_script.py b/chatsky/utils/testing/toy_script.py index a898298eb..f4327e180 100644 --- a/chatsky/utils/testing/toy_script.py +++ b/chatsky/utils/testing/toy_script.py @@ -39,8 +39,11 @@ :meta hide-value: """ -TOY_SCRIPT_KWARGS = {"script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), - "fallback_label": ("greeting_flow", "fallback_node")} +TOY_SCRIPT_KWARGS = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), +} """ # There should be a better description of this Keyword arguments to pass to :py:meth:`~chatsky.pipeline.pipeline.pipeline.Pipeline` in order to diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index 96fba494a..102f7041e 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -1,24 +1,18 @@ import importlib from chatsky.script import Message -from tests.test_utils import get_path_from_tests_to_current_dir from chatsky.pipeline import Pipeline from chatsky.script.core.keywords import RESPONSE, TRANSITIONS import chatsky.script.conditions as cnd -dot_path_to_addon = get_path_from_tests_to_current_dir(__file__, separator=".") - - -def test_pretty_format(): - tutorial_module = importlib.import_module(f"tutorials.{dot_path_to_addon}.5_asynchronous_groups_and_services_full") - tutorial_module.pipeline.pretty_format() - - def test_script_getting_and_setting(): script = {"old_flow": {"": {RESPONSE: lambda _, __: Message(), TRANSITIONS: {"": cnd.true()}}}} pipeline = Pipeline(script=script, start_label=("old_flow", "")) new_script = {"new_flow": {"": {RESPONSE: lambda _, __: Message(), TRANSITIONS: {"": cnd.false()}}}} - pipeline.set_actor(script=new_script, start_label=("new_flow", "")) - assert list(pipeline.script.script.keys())[0] == list(new_script.keys())[0] + # IDE gives the warning that "Property 'script' cannot be set" + # And yet the test still passes. + pipeline.script = new_script + pipeline.start_label = ("new_flow", "") + assert list(pipeline.script.keys())[0] == list(new_script.keys())[0] diff --git a/tests/script/core/test_actor.py b/tests/script/core/test_actor.py index 4c6b2722b..98ef97e78 100644 --- a/tests/script/core/test_actor.py +++ b/tests/script/core/test_actor.py @@ -74,24 +74,20 @@ async def test_actor(): start_label=("flow", "node1"), ) ctx = Context() - await pipeline.actor(pipeline, ctx) + await pipeline.actor(ctx, pipeline) raise Exception("can not be passed: fail of response returned Callable") except ValueError: pass # empty ctx stability - pipeline = Pipeline( - script={"flow": {"node1": {TRANSITIONS: {"node1": true()}}}}, start_label=("flow", "node1") - ) + pipeline = Pipeline(script={"flow": {"node1": {TRANSITIONS: {"node1": true()}}}}, start_label=("flow", "node1")) ctx = Context() - await pipeline.actor(pipeline, ctx) + await pipeline.actor(ctx, pipeline) # fake label stability - pipeline = Pipeline( - script={"flow": {"node1": {TRANSITIONS: {fake_label: true()}}}}, start_label=("flow", "node1") - ) + pipeline = Pipeline(script={"flow": {"node1": {TRANSITIONS: {fake_label: true()}}}}, start_label=("flow", "node1")) ctx = Context() - await pipeline.actor(pipeline, ctx) + await pipeline.actor(ctx, pipeline) limit_errors = {} From 66909f3611f737637a54039dd61152a566e96a54 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 13:24:15 +0500 Subject: [PATCH 17/86] mistake fixed + fully removed from_script() and set_actor() --- chatsky/pipeline/pipeline/pipeline.py | 103 -------------------------- chatsky/pipeline/service/service.py | 2 +- 2 files changed, 1 insertion(+), 104 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 31ea401ba..d81254b56 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -219,109 +219,6 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } - # I know this function will be removed, but for testing I'll keep it for now - @classmethod - def from_script( - cls, - script: Union[Script, Dict], - start_label: NodeLabel2Type, - fallback_label: Optional[NodeLabel2Type] = None, - label_priority: float = 1.0, - condition_handler: Callable = default_condition_handler, - slots: Optional[Union[GroupSlot, Dict]] = None, - parallelize_processing: bool = False, - handlers: Optional[Dict[ActorStage, List[Callable]]] = None, - context_storage: Optional[Union[DBContextStorage, Dict]] = None, - messenger_interface: Optional[MessengerInterface] = CLIMessengerInterface(), - pre_services: Optional[List[Union[Service, ServiceGroup]]] = None, - post_services: Optional[List[Union[Service, ServiceGroup]]] = None, - ) -> "Pipeline": - """ - Pipeline script-based constructor. - It creates :py:class:`~.Actor` object and wraps it with pipeline. - NB! It is generally not designed for projects with complex structure. - :py:class:`~.Service` and :py:class:`~.ServiceGroup` customization - becomes not as obvious as it could be with it. - Should be preferred for simple workflows with Actor auto-execution. - :param script: (required) A :py:class:`~.Script` instance (object or dict). - :param start_label: (required) Actor start label. - :param fallback_label: Actor fallback label. - :param label_priority: Default priority value for all actor :py:const:`labels ` - where there is no priority. Defaults to `1.0`. - :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. - :param slots: Slots configuration. - :param parallelize_processing: This flag determines whether or not the functions - defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections - of the script should be parallelized over respective groups. - :param handlers: This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. - - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. - :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline - or a dict to store dialog :py:class:`~.Context`. - :param messenger_interface: An instance for this pipeline. - :param pre_services: List of :py:data:`~.ServiceBuilder` or - :py:data:`~.ServiceGroupBuilder` that will be executed before Actor. - :type pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] - :param post_services: List of :py:data:`~.ServiceBuilder` or - :py:data:`~.ServiceGroupBuilder` that will be executed after Actor. - It constructs root service group by merging `pre_services` + actor + `post_services`. - :type post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] - """ - pre_services = [] if pre_services is None else pre_services - post_services = [] if post_services is None else post_services - return cls( - pre_services=pre_services, - post_services=post_services, - script=script, - start_label=start_label, - fallback_label=fallback_label, - label_priority=label_priority, - condition_handler=condition_handler, - slots=slots, - parallelize_processing=parallelize_processing, - handlers=handlers, - messenger_interface=messenger_interface, - context_storage=context_storage, - ) - - def set_actor( - self, - script: Union[Script, Dict], - start_label: NodeLabel2Type, - fallback_label: Optional[NodeLabel2Type] = None, - label_priority: float = 1.0, - condition_handler: Callable = default_condition_handler, - handlers: Optional[Dict[ActorStage, List[Callable]]] = None, - ): - """ - Set actor for the current pipeline and conducts necessary checks. - Reset actor to previous if any errors are found. - - :param script: (required) A :py:class:`~.Script` instance (object or dict). - :param start_label: (required) Actor start label. - The start node of :py:class:`~chatsky.script.Script`. The execution begins with it. - :param fallback_label: Actor fallback label. The label of :py:class:`~chatsky.script.Script`. - Dialog comes into that label if all other transitions failed, - or there was an error while executing the scenario. - :param label_priority: Default priority value for all actor :py:const:`labels ` - where there is no priority. Defaults to `1.0`. - :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. - :param handlers: This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. - - - key :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - - value List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. - """ - self.actor = Actor( - script=script, - start_label=start_label, - fallback_label=fallback_label, - label_priority=label_priority, - condition_handler=condition_handler, - handlers=handlers, - ) - @classmethod def from_dict(cls, dictionary: dict) -> "Pipeline": """ diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index c8495505f..4f5db3ecd 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -134,7 +134,7 @@ def inner(handler: ServiceFunction) -> Service: before_handler=before_handler, after_handler=after_handler, timeout=timeout, - asynchronous=asynchronous, + requested_async_flag=asynchronous, start_condition=start_condition, name=name, ) From 9773e63a9e259087db3eb5c3bd6c6134b46f60a8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 14:09:33 +0500 Subject: [PATCH 18/86] changed components into pre- and post-services for Pipeline initialization --- .../3_pipeline_dict_with_services_basic.py | 11 +-- .../3_pipeline_dict_with_services_full.py | 16 ++--- .../pipeline/4_groups_and_conditions_basic.py | 10 ++- .../pipeline/4_groups_and_conditions_full.py | 18 +++-- ..._asynchronous_groups_and_services_basic.py | 5 +- ...5_asynchronous_groups_and_services_full.py | 27 ++++---- tutorials/pipeline/6_extra_handlers_basic.py | 67 +++++++++---------- tutorials/pipeline/6_extra_handlers_full.py | 15 ++--- .../7_extra_handlers_and_extensions.py | 5 +- tutorials/stats/1_extractor_functions.py | 9 +-- tutorials/stats/2_pipeline_integration.py | 39 +++++------ 11 files changed, 90 insertions(+), 132 deletions(-) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index a4ad6507e..2b11aed20 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -76,16 +76,11 @@ def postprocess(_): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - { - "handler": prepreprocess, - }, + "pre-services": [ + {"handler": prepreprocess}, preprocess, - ACTOR, - Service( - handler=postprocess, - ), ], + "post-services": Service(handler=postprocess), } # %% diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index 2759a502a..310b73c13 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -151,22 +151,14 @@ def postprocess(ctx: Context, pl: Pipeline): # `prompt_request` - a string that will be displayed before user input # `prompt_response` - an output prefix string "context_storage": {}, - "components": [ + "pre-services": [ { - "handler": { - "handler": prepreprocess, - "name": "silly_service_name", - }, + "handler": {prepreprocess}, "name": "preprocessor", - }, # This service will be named `preprocessor` - # handler name will be overridden + }, preprocess, - ACTOR, - Service( - handler=postprocess, - name="postprocessor", - ), ], + "post-services": Service(handler=postprocess, name="postprocessor"), } diff --git a/tutorials/pipeline/4_groups_and_conditions_basic.py b/tutorials/pipeline/4_groups_and_conditions_basic.py index d791c0e11..9998e1f63 100644 --- a/tutorials/pipeline/4_groups_and_conditions_basic.py +++ b/tutorials/pipeline/4_groups_and_conditions_basic.py @@ -96,12 +96,10 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - Service( - handler=always_running_service, - name="always_running_service", - ), - ACTOR, + "pre-services": Service( + handler=always_running_service, name="always_running_service" + ), + "post-services": [ Service( handler=never_running_service, start_condition=not_condition( diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index b0190b54d..0af267ddc 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -34,7 +34,6 @@ logger = logging.getLogger(__name__) - # %% [markdown] """ Pipeline can contain not only single services, but also service groups. @@ -170,15 +169,14 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - [ - simple_service, # This simple service - # will be named `simple_service_0` - simple_service, # This simple service - # will be named `simple_service_1` - ], # Despite this is the unnamed service group in the root - # service group, it will be named `service_group_0` - ACTOR, + "pre-services": [ + simple_service, # This simple service + # will be named `simple_service_0` + simple_service, # This simple service + # will be named `simple_service_1` + ], # Despite this is the unnamed service group in the root + # service group, it will be named `service_group_0` + "post-services": [ ServiceGroup( name="named_group", components=[ diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py index 9876290e9..0912d0685 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py @@ -50,10 +50,7 @@ async def time_consuming_service(_): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - [time_consuming_service for _ in range(0, 10)], - ACTOR, - ], + "pre-services": [time_consuming_service for _ in range(0, 10)], } # %% diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index fbe707ff7..a5a9a4b67 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -127,20 +127,19 @@ def context_printing_service(ctx: Context): "fallback_label": ("greeting_flow", "fallback_node"), "optimization_warnings": True, # There are no warnings - pipeline is well-optimized - "components": [ - ServiceGroup( - name="balanced_group", - asynchronous=False, - components=[ - simple_asynchronous_service, - ServiceGroup( - timeout=0.02, - components=[time_consuming_service for _ in range(0, 6)], - ), - simple_asynchronous_service, - ], - ), - ACTOR, + "pre-services": ServiceGroup( + name="balanced_group", + asynchronous=False, + components=[ + simple_asynchronous_service, + ServiceGroup( + timeout=0.02, + components=[time_consuming_service for _ in range(0, 6)], + ), + simple_asynchronous_service, + ], + ), + "post-services": [ [meta_web_querying_service(photo) for photo in range(1, 16)], context_printing_service, ], diff --git a/tutorials/pipeline/6_extra_handlers_basic.py b/tutorials/pipeline/6_extra_handlers_basic.py index 11fe52cfe..8eb2d57d0 100644 --- a/tutorials/pipeline/6_extra_handlers_basic.py +++ b/tutorials/pipeline/6_extra_handlers_basic.py @@ -82,41 +82,38 @@ def logging_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - ServiceGroup( - before_handler=[collect_timestamp_before], - after_handler=[collect_timestamp_after], - components=[ - { - "handler": heavy_service, - "before_handler": [collect_timestamp_before], - "after_handler": [collect_timestamp_after], - }, - { - "handler": heavy_service, - "before_handler": [collect_timestamp_before], - "after_handler": [collect_timestamp_after], - }, - { - "handler": heavy_service, - "before_handler": [collect_timestamp_before], - "after_handler": [collect_timestamp_after], - }, - { - "handler": heavy_service, - "before_handler": [collect_timestamp_before], - "after_handler": [collect_timestamp_after], - }, - { - "handler": heavy_service, - "before_handler": [collect_timestamp_before], - "after_handler": [collect_timestamp_after], - }, - ], - ), - ACTOR, - logging_service, - ], + "pre-services": ServiceGroup( + before_handler=[collect_timestamp_before], + after_handler=[collect_timestamp_after], + components=[ + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + ], + ), + "post-services": logging_service, } # %% diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index dbc717b59..53dbdc8d5 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -172,15 +172,12 @@ def logging_service(ctx: Context, _, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - ServiceGroup( - before_handler=[time_measure_before_handler], - after_handler=[time_measure_after_handler], - components=[heavy_service for _ in range(0, 5)], - ), - ACTOR, - logging_service, - ], + "pre-services": ServiceGroup( + before_handler=[time_measure_before_handler], + after_handler=[time_measure_after_handler], + components=[heavy_service for _ in range(0, 5)], + ), + "post-services": logging_service, } # %% diff --git a/tutorials/pipeline/7_extra_handlers_and_extensions.py b/tutorials/pipeline/7_extra_handlers_and_extensions.py index 619e2aaed..e07f081d9 100644 --- a/tutorials/pipeline/7_extra_handlers_and_extensions.py +++ b/tutorials/pipeline/7_extra_handlers_and_extensions.py @@ -124,10 +124,7 @@ async def long_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - [long_service for _ in range(0, 25)], - ACTOR, - ], + "pre-services": [long_service for _ in range(0, 25)], } # %% diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 1597df5ea..cec9abdc1 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -123,13 +123,8 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - heavy_service, - Service( - handler=ACTOR, - after_handler=[default_extractors.get_current_label], - ), - ], + "pre-services": heavy_service, + "after_actor": [default_extractors.get_current_label], } ) diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index 0877aa6c0..01825d57a 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -100,29 +100,22 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "components": [ - ServiceGroup( - before_handler=[default_extractors.get_timing_before], - after_handler=[ - get_service_state, - default_extractors.get_timing_after, - ], - components=[ - {"handler": heavy_service}, - {"handler": heavy_service}, - ], - ), - Service( - handler=ACTOR, - before_handler=[ - default_extractors.get_timing_before, - ], - after_handler=[ - get_service_state, - default_extractors.get_current_label, - default_extractors.get_timing_after, - ], - ), + "pre-services": ServiceGroup( + before_handler=[default_extractors.get_timing_before], + after_handler=[ + get_service_state, + default_extractors.get_timing_after, + ], + components=[ + {"handler": heavy_service}, + {"handler": heavy_service}, + ], + ), + "before_actor": [default_extractors.get_timing_before], + "after_actor": [ + get_service_state, + default_extractors.get_current_label, + default_extractors.get_timing_after, ], } ) From baa16984e0f89a62d97e169c3b4bfeeed63f3011 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 14:19:41 +0500 Subject: [PATCH 19/86] lint --- chatsky/pipeline/pipeline/pipeline.py | 13 +++---------- chatsky/pipeline/service/group.py | 7 ++----- tests/pipeline/test_pipeline.py | 2 -- .../pipeline/3_pipeline_dict_with_services_basic.py | 2 +- .../pipeline/3_pipeline_dict_with_services_full.py | 2 +- tutorials/pipeline/4_groups_and_conditions_basic.py | 1 - tutorials/pipeline/4_groups_and_conditions_full.py | 1 - .../5_asynchronous_groups_and_services_basic.py | 2 +- .../5_asynchronous_groups_and_services_full.py | 4 +--- tutorials/pipeline/6_extra_handlers_basic.py | 5 +---- tutorials/pipeline/6_extra_handlers_full.py | 4 +--- .../pipeline/7_extra_handlers_and_extensions.py | 2 -- tutorials/stats/1_extractor_functions.py | 7 ++----- tutorials/stats/2_pipeline_integration.py | 8 +++----- 14 files changed, 16 insertions(+), 44 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index d81254b56..5fe8742be 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -16,8 +16,8 @@ import asyncio import logging -from typing import Union, List, Dict, Optional, Hashable, Callable, Any -from pydantic import BaseModel, Field, model_validator, computed_field, field_validator +from typing import Union, List, Dict, Optional, Hashable, Callable +from pydantic import BaseModel, Field, model_validator, computed_field from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -27,11 +27,9 @@ from chatsky.messengers.console import CLIMessengerInterface from chatsky.messengers.common import MessengerInterface from chatsky.slots.slots import GroupSlot -from chatsky.pipeline.service.service import Service from chatsky.pipeline.service.group import ServiceGroup -from chatsky.pipeline.service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler +from chatsky.pipeline.service.extra import ComponentExtraHandler from ..types import ( - ServiceFunction, GlobalExtraHandlerType, ExtraHandlerFunction, # Everything breaks without this import, even though it's unused. @@ -42,11 +40,6 @@ from .utils import finalize_service_group from chatsky.pipeline.pipeline.actor import Actor, default_condition_handler -""" -if TYPE_CHECKING: - from .. import Service - from ..service.group import ServiceGroup -""" logger = logging.getLogger(__name__) ACTOR = "ACTOR" diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 3f6c356e5..d28cc9243 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,21 +11,18 @@ from __future__ import annotations import asyncio import logging -from typing import Optional, List, Union, Awaitable, TYPE_CHECKING, Any, Callable, Dict -from pydantic import model_validator, field_validator +from typing import List, Union, Awaitable, TYPE_CHECKING, Any +from pydantic import model_validator from chatsky.script import Context -from .extra import ComponentExtraHandler from ..pipeline.actor import Actor from ..pipeline.component import PipelineComponent from ..types import ( - StartConditionCheckerFunction, ComponentExecutionState, GlobalExtraHandlerType, ExtraHandlerConditionFunction, ExtraHandlerFunction, - ExtraHandlerType, ) from .service import Service diff --git a/tests/pipeline/test_pipeline.py b/tests/pipeline/test_pipeline.py index 102f7041e..8d385e2c8 100644 --- a/tests/pipeline/test_pipeline.py +++ b/tests/pipeline/test_pipeline.py @@ -1,5 +1,3 @@ -import importlib - from chatsky.script import Message from chatsky.pipeline import Pipeline from chatsky.script.core.keywords import RESPONSE, TRANSITIONS diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index 2b11aed20..a92244092 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -17,7 +17,7 @@ # %% import logging -from chatsky.pipeline import Service, Pipeline, ACTOR +from chatsky.pipeline import Service, Pipeline from chatsky.utils.testing.common import ( check_happy_path, diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index 310b73c13..68216b621 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -20,7 +20,7 @@ from chatsky.script import Context from chatsky.messengers.console import CLIMessengerInterface -from chatsky.pipeline import Service, Pipeline, ServiceRuntimeInfo, ACTOR +from chatsky.pipeline import Service, Pipeline, ServiceRuntimeInfo from chatsky.utils.testing.common import ( check_happy_path, is_interactive_mode, diff --git a/tutorials/pipeline/4_groups_and_conditions_basic.py b/tutorials/pipeline/4_groups_and_conditions_basic.py index 9998e1f63..4572fb808 100644 --- a/tutorials/pipeline/4_groups_and_conditions_basic.py +++ b/tutorials/pipeline/4_groups_and_conditions_basic.py @@ -21,7 +21,6 @@ not_condition, service_successful_condition, ServiceRuntimeInfo, - ACTOR, ) from chatsky.utils.testing.common import ( diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index 0af267ddc..76e948be5 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -22,7 +22,6 @@ service_successful_condition, all_condition, ServiceRuntimeInfo, - ACTOR, ) from chatsky.utils.testing.common import ( diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py index 0912d0685..796c334c3 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py @@ -14,7 +14,7 @@ # %% import asyncio -from chatsky.pipeline import Pipeline, ACTOR +from chatsky.pipeline import Pipeline from chatsky.utils.testing.common import ( is_interactive_mode, diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index a5a9a4b67..bc0f63edf 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -19,10 +19,8 @@ import logging import urllib.request +from chatsky.pipeline import ServiceGroup, Pipeline, ServiceRuntimeInfo from chatsky.script import Context - -from chatsky.pipeline import ServiceGroup, Pipeline, ServiceRuntimeInfo, ACTOR - from chatsky.utils.testing.common import ( check_happy_path, is_interactive_mode, diff --git a/tutorials/pipeline/6_extra_handlers_basic.py b/tutorials/pipeline/6_extra_handlers_basic.py index 8eb2d57d0..314e85e91 100644 --- a/tutorials/pipeline/6_extra_handlers_basic.py +++ b/tutorials/pipeline/6_extra_handlers_basic.py @@ -18,15 +18,12 @@ import random from datetime import datetime -from chatsky.script import Context - from chatsky.pipeline import ( Pipeline, ServiceGroup, ExtraHandlerRuntimeInfo, - ACTOR, ) - +from chatsky.script import Context from chatsky.utils.testing.common import ( check_happy_path, is_interactive_mode, diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index 53dbdc8d5..b2c6744a1 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -17,7 +17,6 @@ from datetime import datetime import psutil -from chatsky.script import Context from chatsky.pipeline import ( Pipeline, @@ -25,9 +24,8 @@ to_service, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, - ACTOR, ) - +from chatsky.script import Context from chatsky.utils.testing.common import ( check_happy_path, is_interactive_mode, diff --git a/tutorials/pipeline/7_extra_handlers_and_extensions.py b/tutorials/pipeline/7_extra_handlers_and_extensions.py index e07f081d9..4d9907627 100644 --- a/tutorials/pipeline/7_extra_handlers_and_extensions.py +++ b/tutorials/pipeline/7_extra_handlers_and_extensions.py @@ -25,9 +25,7 @@ GlobalExtraHandlerType, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, - ACTOR, ) - from chatsky.utils.testing.common import ( check_happy_path, is_interactive_mode, diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index cec9abdc1..545822269 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -46,18 +46,15 @@ # %% import asyncio -from chatsky.script import Context from chatsky.pipeline import ( Pipeline, - ACTOR, - Service, ExtraHandlerRuntimeInfo, to_service, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH +from chatsky.script import Context from chatsky.stats import OtelInstrumentor, default_extractors from chatsky.utils.testing import is_interactive_mode, check_happy_path - +from chatsky.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH # %% [markdown] """ diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index 01825d57a..7fd9eeb8c 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -29,24 +29,22 @@ # %% import asyncio -from chatsky.script import Context from chatsky.pipeline import ( Pipeline, - ACTOR, - Service, ExtraHandlerRuntimeInfo, ServiceGroup, GlobalExtraHandlerType, ) -from chatsky.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH +from chatsky.script import Context +from chatsky.stats import OTLPLogExporter, OTLPSpanExporter from chatsky.stats import ( OtelInstrumentor, set_logger_destination, set_tracer_destination, ) -from chatsky.stats import OTLPLogExporter, OTLPSpanExporter from chatsky.stats import default_extractors from chatsky.utils.testing import is_interactive_mode, check_happy_path +from chatsky.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH # %% set_logger_destination(OTLPLogExporter("grpc://localhost:4317", insecure=True)) From e33bb164172c006c45a0c8936be7858a83127b59 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 14:44:46 +0500 Subject: [PATCH 20/86] minor fix --- chatsky/pipeline/service/extra.py | 3 ++- chatsky/pipeline/service/group.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 22fe444cd..0e18b2143 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -55,9 +55,10 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @classmethod # Here Script class has "@validate_call". Is it needed here? def functions_constructor(cls, data: Any): - result = data.copy() if not isinstance(data, dict): result = {"functions": data} + else: + result = data.copy() # Now it's definitely a dictionary. if ("functions" in result) and (not isinstance(result["functions"], list)): result["functions"] = [result["functions"]] diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index d28cc9243..0c5ee4f80 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -77,9 +77,10 @@ def components_constructor(cls, data: Any): # Question: I don't think shallow copy() could be a problem for this, right? # Pydantic is already rather recursively checking types. # print(data) - result = data.copy() if not isinstance(data, dict): result = {"components": data} + else: + result = data.copy() # When it's a dictionary, data is cast to a list. # We don't need to check if it's a list of Services or anything else: Pydantic does that for us. if ("components" in result) and (not isinstance(result["components"], list)): From 43de1db573ee52aa65a8ebeb518b3c359170eb45 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 15:05:04 +0500 Subject: [PATCH 21/86] minor mistake fixed --- chatsky/pipeline/pipeline/component.py | 1 + tutorials/pipeline/5_asynchronous_groups_and_services_full.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 658e4128a..10551ffe6 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -67,6 +67,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) timeout: Optional[float] = None + # The user sees this name right now, this has to be changed. It's just counter-intuitive. requested_async_flag: Optional[bool] = None calculated_async_flag: bool = False # Is this field really Optional[]? Also, is the Field(default=) done right? diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index bc0f63edf..f81262980 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -127,7 +127,7 @@ def context_printing_service(ctx: Context): # There are no warnings - pipeline is well-optimized "pre-services": ServiceGroup( name="balanced_group", - asynchronous=False, + requested_async_flag=False, components=[ simple_asynchronous_service, ServiceGroup( From fe78e12d8f4717adf21e42ffd39e003bfdeb26ab Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 22 Jul 2024 15:22:24 +0500 Subject: [PATCH 22/86] found and fixed minor bug --- chatsky/pipeline/service/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 0c5ee4f80..14df5e9e8 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -141,7 +141,7 @@ def log_optimization_warnings(self): :return: `None` """ for service in self.components: - if isinstance(service, Service): + if not isinstance(service, ServiceGroup): if ( service.calculated_async_flag and service.requested_async_flag is not None From 5753de92afa8c9a4f88a87ca645b7734503b7092 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 24 Jul 2024 15:32:15 +0500 Subject: [PATCH 23/86] removed to_service() and Optional[] where possible, also removed computed_fields from Pipeline, they're not working as intended --- chatsky/pipeline/__init__.py | 2 +- chatsky/pipeline/pipeline/actor.py | 5 +-- chatsky/pipeline/pipeline/component.py | 8 +--- chatsky/pipeline/pipeline/pipeline.py | 44 +++++++++++---------- chatsky/pipeline/service/group.py | 11 ++---- chatsky/pipeline/service/service.py | 33 +--------------- tests/script/core/test_actor.py | 5 ++- tutorials/pipeline/6_extra_handlers_full.py | 26 ++++++------ tutorials/stats/1_extractor_functions.py | 10 +++-- tutorials/stats/2_pipeline_integration.py | 13 +++--- 10 files changed, 62 insertions(+), 95 deletions(-) diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 5e5ed047f..76d664691 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -26,4 +26,4 @@ from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from .service.group import ServiceGroup -from .service.service import Service, to_service +from .service.service import Service diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 60e86b249..c116584ff 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -92,7 +92,7 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) - handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default_factory=dict) + handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) _clean_turn_cache: Optional[bool] = True # Making a 'computed field' for this feels overkill, a 'private' field like this is probably fine? @@ -117,9 +117,6 @@ def actor_validator(self): if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: raise ValueError(f"Unknown fallback_label={self.fallback_label}") - # This line should be removed right after removing from_script() method. - self.handlers = {} if self.handlers is None else self.handlers - # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! self._clean_turn_cache = True return self diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 10551ffe6..14379ab82 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -61,24 +61,18 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo :param path: Separated by dots path to component, is universally unique. """ - # I think before this you could pass a List[ExtraHandlerFunction] which would be turned into a ComponentExtraHandler - # Now you can't, option removed. Is that correct? Seems easy to do with field_validator. - # Possible TODO: Implement a Pydantic field_validator here for keeping that option. before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) timeout: Optional[float] = None # The user sees this name right now, this has to be changed. It's just counter-intuitive. requested_async_flag: Optional[bool] = None calculated_async_flag: bool = False - # Is this field really Optional[]? Also, is the Field(default=) done right? - start_condition: Optional[StartConditionCheckerFunction] = Field(default=always_start_condition) + start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) name: Optional[str] = None path: Optional[str] = None @model_validator(mode="after") def pipeline_component_validator(self): - self.start_condition = always_start_condition if self.start_condition is None else self.start_condition - if self.name is not None and (self.name == "" or "." in self.name): raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {self.name})!") diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 5fe8742be..42a8dfe62 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -95,21 +95,31 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) slots: Optional[Union[GroupSlot, Dict]] = None - handlers: Optional[Dict[ActorStage, List[Callable]]] = Field(default_factory=dict) + handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) - context_storage: Optional[Union[DBContextStorage, Dict]] = None + context_storage: Union[DBContextStorage, Dict] = Field(default_factory=dict) before_handler: ComponentExtraHandler = Field(default_factory=list) after_handler: ComponentExtraHandler = Field(default_factory=list) timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False # TO-DO: Remove/change parameters below (if possible) - _services_pipeline: Optional[ServiceGroup] + actor: Optional[Actor] = None + _services_pipeline: Optional[ServiceGroup] = None _clean_turn_cache: Optional[bool] - @computed_field(repr=False) - def _services_pipeline(self) -> ServiceGroup: - components = [*self.pre_services, self.actor, *self.post_services] + def _create_actor(self) -> Actor: + return Actor( + script=self.script, + start_label=self.start_label, + fallback_label=self.fallback_label, + label_priority=self.label_priority, + condition_handler=self.condition_handler, + handlers=self.handlers, + ) + + def _create_pipeline_services(self) -> ServiceGroup: + components = [self.pre_services, self.actor, self.post_services] services_pipeline = ServiceGroup( components=components, before_handler=self.before_handler, @@ -120,17 +130,6 @@ def _services_pipeline(self) -> ServiceGroup: services_pipeline.path = ".pipeline" return services_pipeline - @computed_field(repr=False) - def actor(self) -> Actor: - return Actor( - script=self.script, - start_label=self.start_label, - fallback_label=self.fallback_label, - label_priority=self.label_priority, - condition_handler=self.condition_handler, - handlers=self.handlers, - ) - @model_validator(mode="after") def pipeline_init(self): """# I wonder if I could make actor itself a @computed_field, but I'm not sure that would work. @@ -138,11 +137,16 @@ def pipeline_init(self): # Same goes for @cached_property. Would @property work? self.actor = self._set_actor""" - # These lines should be removed right after removing the from_script() method. - self.context_storage = {} if self.context_storage is None else self.context_storage - # Same here, but this line creates a Pydantic error now, though it shouldn't have. + # Should do this in a Pydantic Field self.slots = GroupSlot.model_validate(self.slots) if self.slots is not None else None + # computed_field is just not viable, they get called multiple times. + # Could maybe do this with a default_factory, but such function will require + # being defined before Pipeline + self.actor = self._create_actor() + + self._services_pipeline = self._create_pipeline_services() + finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) # This could be removed. diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 14df5e9e8..e1a1c88dc 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -57,9 +57,6 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr :param name: Requested group name. """ - # If this is a list of PipelineComponents, why would the program know this is supposed to be a Service in the end? - # It's kind of logical it would try to match the best one fitting, but there are no guarantees, right? - # components: List[PipelineComponent] components: List[ Union[ Actor, @@ -74,17 +71,15 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr @classmethod # Here Script class has "@validate_call". Is it needed here? def components_constructor(cls, data: Any): - # Question: I don't think shallow copy() could be a problem for this, right? - # Pydantic is already rather recursively checking types. - # print(data) if not isinstance(data, dict): result = {"components": data} else: result = data.copy() - # When it's a dictionary, data is cast to a list. - # We don't need to check if it's a list of Services or anything else: Pydantic does that for us. + # print(result) + if ("components" in result) and (not isinstance(result["components"], list)): result["components"] = [result["components"]] + # print(result) return result # Is there a better way to do this? calculated_async_flag is exposed to the user right now. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 4f5db3ecd..0079f005a 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,17 +13,15 @@ from __future__ import annotations import logging import inspect -from typing import Optional, TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any from pydantic import model_validator from chatsky.script import Context -from .extra import ComponentExtraHandler from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from ..types import ( ServiceFunction, - StartConditionCheckerFunction, ) from ..pipeline.component import PipelineComponent @@ -111,32 +109,3 @@ def info_dict(self) -> dict: service_representation = "[Unknown]" representation.update({"handler": service_representation}) return representation - - -# If this function will continue existing. -def to_service( - before_handler: Optional[ComponentExtraHandler] = None, - after_handler: Optional[ComponentExtraHandler] = None, - timeout: Optional[int] = None, - asynchronous: Optional[bool] = None, - start_condition: Optional[StartConditionCheckerFunction] = None, - name: Optional[str] = None, -): - """ - Function for decorating a function as a Service. - Returns a Service, constructed from this function (taken as a handler). - All arguments are passed directly to `Service` constructor. - """ - - def inner(handler: ServiceFunction) -> Service: - return Service( - handler=handler, - before_handler=before_handler, - after_handler=after_handler, - timeout=timeout, - requested_async_flag=asynchronous, - start_condition=start_condition, - name=name, - ) - - return inner diff --git a/tests/script/core/test_actor.py b/tests/script/core/test_actor.py index 98ef97e78..b840fdc3e 100644 --- a/tests/script/core/test_actor.py +++ b/tests/script/core/test_actor.py @@ -1,6 +1,6 @@ # %% import pytest -from chatsky.pipeline import Pipeline +from chatsky.pipeline import Pipeline, ComponentExecutionState from chatsky.script import ( TRANSITIONS, RESPONSE, @@ -75,8 +75,9 @@ async def test_actor(): ) ctx = Context() await pipeline.actor(ctx, pipeline) + assert pipeline.actor.get_state(ctx) is not ComponentExecutionState.FAILED raise Exception("can not be passed: fail of response returned Callable") - except ValueError: + except AssertionError: pass # empty ctx stability diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index b2c6744a1..f4406f5e2 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -21,9 +21,9 @@ from chatsky.pipeline import ( Pipeline, ServiceGroup, - to_service, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, + Service, ) from chatsky.script import Context from chatsky.utils.testing.common import ( @@ -69,6 +69,7 @@ 1. Directly in constructor - by adding extra handlers to `before_handler` or `after_handler` constructor parameter. +# Needs to be changed! 'to_service' doesn't exist anymore! 2. (Services only) `to_service` decorator - transforms function to service with extra handlers from `before_handler` and `after_handler` arguments. @@ -146,26 +147,29 @@ def json_converter_after_handler(ctx, _, info): # %% -@to_service( - before_handler=[time_measure_before_handler, ram_measure_before_handler], - after_handler=[time_measure_after_handler, ram_measure_after_handler], -) -def heavy_service(ctx: Context): +def heavy_function(ctx: Context): memory_heap[ctx.last_request.text] = [ random.randint(0, num) for num in range(0, 1000) ] -@to_service( - before_handler=[json_converter_before_handler], - after_handler=[json_converter_after_handler], -) -def logging_service(ctx: Context, _, info: ServiceRuntimeInfo): +def logging_function(ctx: Context, _, info: ServiceRuntimeInfo): str_misc = ctx.misc[f"{info.name}-str"] assert isinstance(str_misc, str) print(f"Stringified misc: {str_misc}") +heavy_service = Service( + handler=heavy_function, + before_handler=[time_measure_before_handler, ram_measure_before_handler], + after_handler=[time_measure_after_handler, ram_measure_after_handler], +) +logging_service = Service( + handler=logging_function, + before_handler=[json_converter_before_handler], + after_handler=[json_converter_after_handler], +) + pipeline_dict = { "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 545822269..167d880f6 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -49,7 +49,7 @@ from chatsky.pipeline import ( Pipeline, ExtraHandlerRuntimeInfo, - to_service, + Service, ) from chatsky.script import Context from chatsky.stats import OtelInstrumentor, default_extractors @@ -108,7 +108,6 @@ async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # %% # configure `get_service_state` to run after the `heavy_service` -@to_service(after_handler=[get_service_state]) async def heavy_service(ctx: Context): _ = ctx # get something from ctx if needed await asyncio.sleep(0.02) @@ -120,8 +119,11 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": heavy_service, - "after_actor": [default_extractors.get_current_label], + "pre-services": Service( + handler=heavy_service, after_handler=[get_service_state] + ), + # TODO: Change this to add_extra_handler() from PipelineComponent + "after_handler": [default_extractors.get_current_label], } ) diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index 7fd9eeb8c..208d7a261 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -109,14 +109,15 @@ async def heavy_service(ctx: Context): {"handler": heavy_service}, ], ), - "before_actor": [default_extractors.get_timing_before], - "after_actor": [ - get_service_state, - default_extractors.get_current_label, - default_extractors.get_timing_after, - ], } ) +# These are Extra Handlers for Actor. +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, get_service_state) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_current_label) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after) + +# These are global Extra Handlers for Pipeline. pipeline.add_global_handler( GlobalExtraHandlerType.BEFORE_ALL, default_extractors.get_timing_before ) From 58af2ca9ba40dabad0324b9b3623c6464630375d Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 24 Jul 2024 16:25:22 +0500 Subject: [PATCH 24/86] replaced Pipeline's from_dict() with model_validate() --- chatsky/pipeline/pipeline/pipeline.py | 8 -------- tests/pipeline/test_validation.py | 0 tutorials/pipeline/3_pipeline_dict_with_services_basic.py | 2 +- tutorials/pipeline/3_pipeline_dict_with_services_full.py | 2 +- tutorials/pipeline/4_groups_and_conditions_basic.py | 2 +- tutorials/pipeline/4_groups_and_conditions_full.py | 2 +- .../pipeline/5_asynchronous_groups_and_services_basic.py | 2 +- .../pipeline/5_asynchronous_groups_and_services_full.py | 2 +- tutorials/stats/1_extractor_functions.py | 2 +- tutorials/stats/2_pipeline_integration.py | 2 +- utils/stats/sample_data_provider.py | 2 +- 11 files changed, 9 insertions(+), 17 deletions(-) create mode 100644 tests/pipeline/test_validation.py diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 42a8dfe62..e056963d4 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -216,14 +216,6 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } - @classmethod - def from_dict(cls, dictionary: dict) -> "Pipeline": - """ - Pipeline dictionary-based constructor. - Dictionary should have the fields defined in Pipeline main constructor, - it will be split and passed to it as `**kwargs`. - """ - return cls(**dictionary) async def _run_pipeline( self, request: Message, ctx_id: Optional[Hashable] = None, update_ctx_misc: Optional[dict] = None diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index a92244092..76250e188 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -84,7 +84,7 @@ def postprocess(_): } # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index 68216b621..a97e01eb9 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -163,7 +163,7 @@ def postprocess(ctx: Context, pl: Pipeline): # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/pipeline/4_groups_and_conditions_basic.py b/tutorials/pipeline/4_groups_and_conditions_basic.py index 4572fb808..96ca2f08a 100644 --- a/tutorials/pipeline/4_groups_and_conditions_basic.py +++ b/tutorials/pipeline/4_groups_and_conditions_basic.py @@ -114,7 +114,7 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index 76e948be5..9a8cc585f 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -208,7 +208,7 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): } # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": logging.basicConfig(level=logging.INFO) diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py index 796c334c3..b34988399 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py @@ -54,7 +54,7 @@ async def time_consuming_service(_): } # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index f81262980..3a0220bfc 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -144,7 +144,7 @@ def context_printing_service(ctx: Context): } # %% -pipeline = Pipeline.from_dict(pipeline_dict) +pipeline = Pipeline.model_validate(pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 167d880f6..dc2f2a4ff 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -114,7 +114,7 @@ async def heavy_service(ctx: Context): # %% -pipeline = Pipeline.from_dict( +pipeline = Pipeline.model_validate( { "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index 208d7a261..fa0ee3474 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -93,7 +93,7 @@ async def heavy_service(ctx: Context): """ # %% -pipeline = Pipeline.from_dict( +pipeline = Pipeline.model_validate( { "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), diff --git a/utils/stats/sample_data_provider.py b/utils/stats/sample_data_provider.py index a880f5d9e..10af87f76 100644 --- a/utils/stats/sample_data_provider.py +++ b/utils/stats/sample_data_provider.py @@ -52,7 +52,7 @@ async def get_confidence(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # %% -pipeline = Pipeline.from_dict( +pipeline = Pipeline.model_validate( { "script": MULTIFLOW_SCRIPT, "start_label": ("root", "start"), From 5a1b19b5726417efdd543433c7cda170d01a21f7 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 25 Jul 2024 19:08:44 +0500 Subject: [PATCH 25/86] minor change --- chatsky/pipeline/__init__.py | 1 + chatsky/pipeline/pipeline/pipeline.py | 12 +----------- chatsky/pipeline/service/service.py | 2 +- tutorials/stats/1_extractor_functions.py | 4 +--- tutorials/stats/2_pipeline_integration.py | 16 ++++++++++++---- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 76d664691..51e4b242c 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -23,6 +23,7 @@ ) from .pipeline.pipeline import Pipeline, ACTOR +from .pipeline.actor import Actor from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from .service.group import ServiceGroup diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index e056963d4..6f80d6c79 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) +# Could be removed, I think. Need to check just in case. ACTOR = "ACTOR" @@ -86,7 +87,6 @@ class Pipeline(BaseModel, arbitrary_types_allowed=True): """ - # Note to self: some testing required to see if [] works as intended. pre_services: ServiceGroup = Field(default_factory=list) post_services: ServiceGroup = Field(default_factory=list) script: Union[Script, Dict] @@ -132,11 +132,6 @@ def _create_pipeline_services(self) -> ServiceGroup: @model_validator(mode="after") def pipeline_init(self): - """# I wonder if I could make actor itself a @computed_field, but I'm not sure that would work. - # What if the cache gets cleaned at some point? Then a new Actor would be created. - # Same goes for @cached_property. Would @property work? - self.actor = self._set_actor""" - # Should do this in a Pydantic Field self.slots = GroupSlot.model_validate(self.slots) if self.slots is not None else None @@ -149,10 +144,6 @@ def pipeline_init(self): finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) - # This could be removed. - if self.actor is None: - raise Exception("Actor wasn't initialized correctly!") - if self.optimization_warnings: self._services_pipeline.log_optimization_warnings() @@ -216,7 +207,6 @@ def info_dict(self) -> dict: "services": [self._services_pipeline.info_dict], } - async def _run_pipeline( self, request: Message, ctx_id: Optional[Hashable] = None, update_ctx_misc: Optional[dict] = None ) -> Context: diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 0079f005a..b26d7a25d 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -61,7 +61,7 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): @classmethod # Here Script class has "@validate_call". Is it needed here? def handler_constructor(cls, data: Any): - if not isinstance(data, dict) and not isinstance(data, list): + if not isinstance(data, dict): return {"handler": data} return data diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index dc2f2a4ff..d2d94bb4f 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -122,11 +122,9 @@ async def heavy_service(ctx: Context): "pre-services": Service( handler=heavy_service, after_handler=[get_service_state] ), - # TODO: Change this to add_extra_handler() from PipelineComponent - "after_handler": [default_extractors.get_current_label], } ) - +pipeline.actor.add_extra_handler("BEFORE", default_extractors.get_current_label) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index fa0ee3474..7a633bb1e 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -112,10 +112,18 @@ async def heavy_service(ctx: Context): } ) # These are Extra Handlers for Actor. -pipeline.actor.add_extra_handler(GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before) -pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, get_service_state) -pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_current_label) -pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, get_service_state +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_current_label +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after +) # These are global Extra Handlers for Pipeline. pipeline.add_global_handler( From 72fb11835ac5d9414d51cda5ed315bbefaec7938 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 25 Jul 2024 20:09:20 +0500 Subject: [PATCH 26/86] added to_service back in, not everywhere yet --- chatsky/pipeline/__init__.py | 2 +- chatsky/pipeline/service/group.py | 2 - chatsky/pipeline/service/service.py | 35 +++- tests/pipeline/test_validation.py | 207 +++++++++++++++++++++++ tutorials/stats/1_extractor_functions.py | 10 +- 5 files changed, 246 insertions(+), 10 deletions(-) diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 51e4b242c..3c3483ba4 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -27,4 +27,4 @@ from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler from .service.group import ServiceGroup -from .service.service import Service +from .service.service import Service, to_service diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index e1a1c88dc..cb833bca1 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -75,11 +75,9 @@ def components_constructor(cls, data: Any): result = {"components": data} else: result = data.copy() - # print(result) if ("components" in result) and (not isinstance(result["components"], list)): result["components"] = [result["components"]] - # print(result) return result # Is there a better way to do this? calculated_async_flag is exposed to the user right now. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index b26d7a25d..247ecc0e3 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,17 +13,19 @@ from __future__ import annotations import logging import inspect -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional from pydantic import model_validator from chatsky.script import Context from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async +from chatsky.pipeline import always_start_condition from ..types import ( - ServiceFunction, + ServiceFunction, StartConditionCheckerFunction, ) from ..pipeline.component import PipelineComponent +from .extra import ComponentExtraHandler logger = logging.getLogger(__name__) @@ -109,3 +111,32 @@ def info_dict(self) -> dict: service_representation = "[Unknown]" representation.update({"handler": service_representation}) return representation + + +def to_service( + # These shouldn't be None, I think. + before_handler: Optional[ComponentExtraHandler] = None, + after_handler: Optional[ComponentExtraHandler] = None, + timeout: Optional[int] = None, + asynchronous: Optional[bool] = None, + start_condition: Optional[StartConditionCheckerFunction] = always_start_condition, + name: Optional[str] = None, +): + """ + Function for decorating a function as a Service. + Returns a Service, constructed from this function (taken as a handler). + All arguments are passed directly to `Service` constructor. + """ + + def inner(handler: ServiceFunction) -> Service: + return Service( + handler=handler, + before_handler=before_handler, + after_handler=after_handler, + timeout=timeout, + requested_async_flag=asynchronous, + start_condition=start_condition, + name=name, + ) + + return inner diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index e69de29bb..0f0f164cc 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -0,0 +1,207 @@ +from pydantic import ValidationError +import pytest + +from chatsky.pipeline import ( + Pipeline, + Service, + ServiceGroup, + Actor, + ComponentExtraHandler, + ServiceRuntimeInfo, + BeforeHandler, +) +from chatsky.script import ( + PRE_RESPONSE_PROCESSING, + PRE_TRANSITIONS_PROCESSING, + RESPONSE, + TRANSITIONS, + Context, + Message, + Script, + ConstLabel, +) +from chatsky.script.conditions import exact_match + + +class UserFunctionSamples: + """ + This class contains various examples of user functions along with their signatures. + """ + + @staticmethod + def wrong_param_number(number: int) -> float: + return 8.0 + number + + @staticmethod + def wrong_param_types(number: int, flag: bool) -> float: + return 8.0 + number if flag else 42.1 + + @staticmethod + def wrong_return_type(_: Context, __: Pipeline) -> float: + return 1.0 + + @staticmethod + def correct_service_function_1(_: Context): + pass + + @staticmethod + def correct_service_function_2(_: Context, __: Pipeline): + pass + + @staticmethod + def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo): + pass + + @staticmethod + def correct_label(_: Context, __: Pipeline) -> ConstLabel: + return ("root", "start", 1) + + @staticmethod + def correct_response(_: Context, __: Pipeline) -> Message: + return Message("hi") + + @staticmethod + def correct_condition(_: Context, __: Pipeline) -> bool: + return True + + @staticmethod + def correct_pre_response_processor(_: Context, __: Pipeline) -> None: + pass + + @staticmethod + def correct_pre_transition_processor(_: Context, __: Pipeline) -> None: + pass + + +# Need a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction +class TestServiceValidation: + def test_param_types(self): + # This doesn't work. For some reason any callable can be a ServiceFunction + # Using model_validate doesn't help + with pytest.raises(ValidationError) as e: + Service(handler=UserFunctionSamples.wrong_param_types) + assert e + Service(handler=UserFunctionSamples.correct_service_function_1) + Service(handler=UserFunctionSamples.correct_service_function_2) + Service(handler=UserFunctionSamples.correct_service_function_3) + + def test_param_number(self): + with pytest.raises(ValidationError) as e: + Service(handler=UserFunctionSamples.wrong_param_number) + assert e + + def test_return_type(self): + with pytest.raises(ValidationError) as e: + Service(handler=UserFunctionSamples.wrong_return_type) + assert e + + def test_model_validator(self): + with pytest.raises(ValidationError) as e: + # Can't pass a list to handler, it has to be a single function + Service(handler=[UserFunctionSamples.correct_service_function_2]) + assert e + with pytest.raises(ValidationError) as e: + # 'handler' is a mandatory field + Service(before_handler=UserFunctionSamples.correct_service_function_2) + assert e + with pytest.raises(ValidationError) as e: + # Can't pass None to handler, it has to be a callable function + # Though I wonder if empty Services should be allowed. + # I see no reason to allow it. + Service() + assert e + with pytest.raises(TypeError) as e: + # Python says that two positional arguments were given when only one was expected. + # This happens before Pydantic's validation, so I think there's nothing we can do. + Service(UserFunctionSamples.correct_service_function_1) + assert e + # But it can work like this. + # A single function gets cast to the right dictionary here. + Service.model_validate(UserFunctionSamples.correct_service_function_1) + + +class TestExtraHandlerValidation: + def test_correct_functions(self): + funcs = [UserFunctionSamples.correct_service_function_1, UserFunctionSamples.correct_service_function_2] + handler = BeforeHandler(funcs) + assert handler.functions == funcs + + def test_single_function(self): + single_function = UserFunctionSamples.correct_service_function_1 + handler = BeforeHandler(single_function) + # Checking that a single function is cast to a list within constructor + assert handler.functions == [single_function] + + def test_wrong_inputs(self): + with pytest.raises(ValidationError) as e: + BeforeHandler(1) + assert e + with pytest.raises(ValidationError) as e: + BeforeHandler([1, 2, 3]) + assert e + # Wait, this one works. Why? + with pytest.raises(ValidationError) as e: + BeforeHandler(functions=BeforeHandler([])) + assert e + +class TestServiceGroupValidation: + def test_single_service(self): + func = UserFunctionSamples.correct_service_function_2 + group = ServiceGroup.model_validate(Service(handler=func, after_handler=func)) + assert group.components[0].handler == func + assert group.components[0].after_handler.functions[0] == func + +""" +class TestActorValidation: +class TestPipelineValidation: +""" + + +class TestLabelValidation: + def test_param_number(self): + with pytest.raises(ValidationError, match=r"Found 3 errors:[\w\W]*Incorrect parameter number") as e: + Script( + script={ + "root": { + "start": {TRANSITIONS: {UserFunctionSamples.wrong_param_number: exact_match(Message("hi"))}} + } + } + ) + assert e + + def test_param_types(self): + with pytest.raises(ValidationError, match=r"Found 3 errors:[\w\W]*Incorrect parameter annotation") as e: + Script( + script={ + "root": { + "start": {TRANSITIONS: {UserFunctionSamples.wrong_param_types: exact_match(Message("hi"))}} + } + } + ) + assert e + + def test_return_type(self): + with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Incorrect return type annotation") as e: + Script( + script={ + "root": { + "start": {TRANSITIONS: {UserFunctionSamples.wrong_return_type: exact_match(Message("hi"))}} + } + } + ) + assert e + + def test_flow_name(self): + with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Flow '\w*' cannot be found for label") as e: + Script(script={"root": {"start": {TRANSITIONS: {("other", "start", 1): exact_match(Message("hi"))}}}}) + assert e + + def test_node_name(self): + with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Node '\w*' cannot be found for label") as e: + Script(script={"root": {"start": {TRANSITIONS: {("root", "other", 1): exact_match(Message("hi"))}}}}) + assert e + + def test_correct_script(self): + Script( + script={"root": {"start": {TRANSITIONS: {UserFunctionSamples.correct_label: exact_match(Message("hi"))}}}} + ) diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index d2d94bb4f..498ba2ebf 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -49,7 +49,8 @@ from chatsky.pipeline import ( Pipeline, ExtraHandlerRuntimeInfo, - Service, + GlobalExtraHandlerType, + to_service, Service, ) from chatsky.script import Context from chatsky.stats import OtelInstrumentor, default_extractors @@ -108,6 +109,7 @@ async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # %% # configure `get_service_state` to run after the `heavy_service` +# @to_service(after_handler=[get_service_state]) async def heavy_service(ctx: Context): _ = ctx # get something from ctx if needed await asyncio.sleep(0.02) @@ -119,13 +121,11 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": Service( - handler=heavy_service, after_handler=[get_service_state] - ), + "pre-services": Service(handler=heavy_service, after_handler=[get_service_state]), } ) -pipeline.actor.add_extra_handler("BEFORE", default_extractors.get_current_label) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.BEFORE, default_extractors.get_current_label) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) if is_interactive_mode(): From 87ad9353bf24e5c96eec53568fa7699ab96a1045 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 16:23:54 +0500 Subject: [PATCH 27/86] mistake found, all tests pass except some of my own --- tutorials/stats/1_extractor_functions.py | 4 ++-- tutorials/stats/2_pipeline_integration.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 498ba2ebf..396137a16 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -109,7 +109,7 @@ async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # %% # configure `get_service_state` to run after the `heavy_service` -# @to_service(after_handler=[get_service_state]) +@to_service(after_handler=[get_service_state]) async def heavy_service(ctx: Context): _ = ctx # get something from ctx if needed await asyncio.sleep(0.02) @@ -121,7 +121,7 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": Service(handler=heavy_service, after_handler=[get_service_state]), + "pre_services": heavy_service, } ) diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index 7a633bb1e..928650f36 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -98,7 +98,7 @@ async def heavy_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": ServiceGroup( + "pre_services": ServiceGroup( before_handler=[default_extractors.get_timing_before], after_handler=[ get_service_state, From 740b7113b34605ab4087df211581ada64e9f0a44 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 17:24:24 +0500 Subject: [PATCH 28/86] mistake fixed + strict validation for Pipeline --- chatsky/pipeline/pipeline/pipeline.py | 2 +- tests/pipeline/test_validation.py | 157 +++++++++--------- .../3_pipeline_dict_with_services_basic.py | 4 +- .../3_pipeline_dict_with_services_full.py | 4 +- .../pipeline/4_groups_and_conditions_basic.py | 4 +- .../pipeline/4_groups_and_conditions_full.py | 4 +- ..._asynchronous_groups_and_services_basic.py | 2 +- ...5_asynchronous_groups_and_services_full.py | 4 +- tutorials/pipeline/6_extra_handlers_basic.py | 4 +- tutorials/pipeline/6_extra_handlers_full.py | 4 +- .../7_extra_handlers_and_extensions.py | 2 +- 11 files changed, 93 insertions(+), 98 deletions(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 6f80d6c79..6e162b31a 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -48,7 +48,7 @@ # Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. # Must also add back in 'extra="forbid"', removed for testing. -class Pipeline(BaseModel, arbitrary_types_allowed=True): +class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 0f0f164cc..70c9824b0 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -10,17 +10,8 @@ ServiceRuntimeInfo, BeforeHandler, ) -from chatsky.script import ( - PRE_RESPONSE_PROCESSING, - PRE_TRANSITIONS_PROCESSING, - RESPONSE, - TRANSITIONS, - Context, - Message, - Script, - ConstLabel, -) -from chatsky.script.conditions import exact_match +from chatsky.script import Context +from chatsky.utils.testing import TOY_SCRIPT, TOY_SCRIPT_KWARGS class UserFunctionSamples: @@ -52,26 +43,6 @@ def correct_service_function_2(_: Context, __: Pipeline): def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo): pass - @staticmethod - def correct_label(_: Context, __: Pipeline) -> ConstLabel: - return ("root", "start", 1) - - @staticmethod - def correct_response(_: Context, __: Pipeline) -> Message: - return Message("hi") - - @staticmethod - def correct_condition(_: Context, __: Pipeline) -> bool: - return True - - @staticmethod - def correct_pre_response_processor(_: Context, __: Pipeline) -> None: - pass - - @staticmethod - def correct_pre_transition_processor(_: Context, __: Pipeline) -> None: - pass - # Need a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction class TestServiceValidation: @@ -134,74 +105,98 @@ def test_single_function(self): def test_wrong_inputs(self): with pytest.raises(ValidationError) as e: + # 1 is not a callable BeforeHandler(1) assert e with pytest.raises(ValidationError) as e: + # 'functions' should be a list of ExtraHandlerFunctions BeforeHandler([1, 2, 3]) assert e - # Wait, this one works. Why? + # Wait, this one works. Why? An instance of BeforeHandler is not a function. with pytest.raises(ValidationError) as e: BeforeHandler(functions=BeforeHandler([])) assert e + class TestServiceGroupValidation: def test_single_service(self): func = UserFunctionSamples.correct_service_function_2 + group = ServiceGroup(components=Service(handler=func, after_handler=func)) + assert group.components[0].handler == func + assert group.components[0].after_handler.functions[0] == func + # Same, but with model_validate group = ServiceGroup.model_validate(Service(handler=func, after_handler=func)) assert group.components[0].handler == func assert group.components[0].after_handler.functions[0] == func -""" + def test_several_correct_services(self): + func = UserFunctionSamples.correct_service_function_2 + services = [Service.model_validate(func), Service(handler=func, timeout=10)] + group = ServiceGroup(components=services, timeout=15) + assert group.components == services + assert group.timeout == 15 + assert group.components[0].timeout is None + assert group.components[1].timeout == 10 + + def test_wrong_inputs(self): + with pytest.raises(ValidationError) as e: + # 'components' is a mandatory field + ServiceGroup(before_handler=UserFunctionSamples.correct_service_function_2) + assert e + with pytest.raises(ValidationError) as e: + # 'components' must be a list of PipelineComponents, wrong type + # Though 123 will be cast to a list + ServiceGroup(components=123) + assert e + with pytest.raises(ValidationError) as e: + # The dictionary inside 'components' will check if Actor, Service or ServiceGroup fit the signature, + # but it doesn't fit any of them, so it's just a normal dictionary + ServiceGroup(components={"before_handler": []}) + assert e + with pytest.raises(ValidationError) as e: + # The dictionary inside 'components' will try to get cast to Service and will fail + # But 'components' must be a list of PipelineComponents, so it's just a normal dictionary + ServiceGroup(components={"handler": 123}) + assert e + + +# Testing of node and script validation for actor exist at script/core/test_actor.py class TestActorValidation: -class TestPipelineValidation: -""" + def test_toy_script_actor(self): + Actor(**TOY_SCRIPT_KWARGS) + + def test_wrong_inputs(self): + with pytest.raises(ValidationError) as e: + # 'script' is a mandatory field + Actor(start_label=TOY_SCRIPT_KWARGS["start_label"]) + assert e + with pytest.raises(ValidationError) as e: + # 'start_label' is a mandatory field + Actor(script={}) + assert e + with pytest.raises(ValidationError) as e: + # 'condition_handler' is not an Optional field. + Actor(**TOY_SCRIPT_KWARGS, condition_handler=None) + assert e + with pytest.raises(ValidationError) as e: + # 'handlers' is not an Optional field. + Actor(**TOY_SCRIPT_KWARGS, handlers=None) + assert e + with pytest.raises(ValidationError) as e: + # 'script' must be either a dict or Script instance. + Actor(script=[], start_label=TOY_SCRIPT_KWARGS["start_label"]) + assert e -class TestLabelValidation: - def test_param_number(self): - with pytest.raises(ValidationError, match=r"Found 3 errors:[\w\W]*Incorrect parameter number") as e: - Script( - script={ - "root": { - "start": {TRANSITIONS: {UserFunctionSamples.wrong_param_number: exact_match(Message("hi"))}} - } - } - ) - assert e +# Can't think of any other tests that aren't done in other tests in this file +class TestPipelineValidation: + def test_correct_inputs(self): + Pipeline(**TOY_SCRIPT_KWARGS) + Pipeline.model_validate(TOY_SCRIPT_KWARGS) - def test_param_types(self): - with pytest.raises(ValidationError, match=r"Found 3 errors:[\w\W]*Incorrect parameter annotation") as e: - Script( - script={ - "root": { - "start": {TRANSITIONS: {UserFunctionSamples.wrong_param_types: exact_match(Message("hi"))}} - } - } - ) - assert e + def test_pre_services(self): + with pytest.raises(ValidationError) as e: + # 'pre_services' must be a ServiceGroup + Pipeline(**TOY_SCRIPT_KWARGS, pre_services=123) + assert e - def test_return_type(self): - with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Incorrect return type annotation") as e: - Script( - script={ - "root": { - "start": {TRANSITIONS: {UserFunctionSamples.wrong_return_type: exact_match(Message("hi"))}} - } - } - ) - assert e - - def test_flow_name(self): - with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Flow '\w*' cannot be found for label") as e: - Script(script={"root": {"start": {TRANSITIONS: {("other", "start", 1): exact_match(Message("hi"))}}}}) - assert e - - def test_node_name(self): - with pytest.raises(ValidationError, match=r"Found 1 error:[\w\W]*Node '\w*' cannot be found for label") as e: - Script(script={"root": {"start": {TRANSITIONS: {("root", "other", 1): exact_match(Message("hi"))}}}}) - assert e - - def test_correct_script(self): - Script( - script={"root": {"start": {TRANSITIONS: {UserFunctionSamples.correct_label: exact_match(Message("hi"))}}}} - ) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index 76250e188..659514e42 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -76,11 +76,11 @@ def postprocess(_): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": [ + "pre_services": [ {"handler": prepreprocess}, preprocess, ], - "post-services": Service(handler=postprocess), + "post_services": Service(handler=postprocess), } # %% diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index a97e01eb9..c0680d6a5 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -151,14 +151,14 @@ def postprocess(ctx: Context, pl: Pipeline): # `prompt_request` - a string that will be displayed before user input # `prompt_response` - an output prefix string "context_storage": {}, - "pre-services": [ + "pre_services": [ { "handler": {prepreprocess}, "name": "preprocessor", }, preprocess, ], - "post-services": Service(handler=postprocess, name="postprocessor"), + "post_services": Service(handler=postprocess, name="postprocessor"), } diff --git a/tutorials/pipeline/4_groups_and_conditions_basic.py b/tutorials/pipeline/4_groups_and_conditions_basic.py index 96ca2f08a..072a4323c 100644 --- a/tutorials/pipeline/4_groups_and_conditions_basic.py +++ b/tutorials/pipeline/4_groups_and_conditions_basic.py @@ -95,10 +95,10 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": Service( + "pre_services": Service( handler=always_running_service, name="always_running_service" ), - "post-services": [ + "post_services": [ Service( handler=never_running_service, start_condition=not_condition( diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index 9a8cc585f..dee90b679 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -168,14 +168,14 @@ def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": [ + "pre_services": [ simple_service, # This simple service # will be named `simple_service_0` simple_service, # This simple service # will be named `simple_service_1` ], # Despite this is the unnamed service group in the root # service group, it will be named `service_group_0` - "post-services": [ + "post_services": [ ServiceGroup( name="named_group", components=[ diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py index b34988399..bf82879a4 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_basic.py @@ -50,7 +50,7 @@ async def time_consuming_service(_): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": [time_consuming_service for _ in range(0, 10)], + "pre_services": [time_consuming_service for _ in range(0, 10)], } # %% diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index 3a0220bfc..40100722b 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -125,7 +125,7 @@ def context_printing_service(ctx: Context): "fallback_label": ("greeting_flow", "fallback_node"), "optimization_warnings": True, # There are no warnings - pipeline is well-optimized - "pre-services": ServiceGroup( + "pre_services": ServiceGroup( name="balanced_group", requested_async_flag=False, components=[ @@ -137,7 +137,7 @@ def context_printing_service(ctx: Context): simple_asynchronous_service, ], ), - "post-services": [ + "post_services": [ [meta_web_querying_service(photo) for photo in range(1, 16)], context_printing_service, ], diff --git a/tutorials/pipeline/6_extra_handlers_basic.py b/tutorials/pipeline/6_extra_handlers_basic.py index 314e85e91..4de439271 100644 --- a/tutorials/pipeline/6_extra_handlers_basic.py +++ b/tutorials/pipeline/6_extra_handlers_basic.py @@ -79,7 +79,7 @@ def logging_service(ctx: Context): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": ServiceGroup( + "pre_services": ServiceGroup( before_handler=[collect_timestamp_before], after_handler=[collect_timestamp_after], components=[ @@ -110,7 +110,7 @@ def logging_service(ctx: Context): }, ], ), - "post-services": logging_service, + "post_services": logging_service, } # %% diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index f4406f5e2..2149aa0a0 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -174,12 +174,12 @@ def logging_function(ctx: Context, _, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": ServiceGroup( + "pre_services": ServiceGroup( before_handler=[time_measure_before_handler], after_handler=[time_measure_after_handler], components=[heavy_service for _ in range(0, 5)], ), - "post-services": logging_service, + "post_services": logging_service, } # %% diff --git a/tutorials/pipeline/7_extra_handlers_and_extensions.py b/tutorials/pipeline/7_extra_handlers_and_extensions.py index 4d9907627..c8a96f620 100644 --- a/tutorials/pipeline/7_extra_handlers_and_extensions.py +++ b/tutorials/pipeline/7_extra_handlers_and_extensions.py @@ -122,7 +122,7 @@ async def long_service(_, __, info: ServiceRuntimeInfo): "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), - "pre-services": [long_service for _ in range(0, 25)], + "pre_services": [long_service for _ in range(0, 25)], } # %% From f3b4b076506f53686cd68a8239949b887750eb2f Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 17:55:31 +0500 Subject: [PATCH 29/86] restored to_service everywhere, finished drafting new tests, some minor changes --- chatsky/pipeline/__init__.py | 2 +- chatsky/pipeline/pipeline/pipeline.py | 19 ++-------- chatsky/pipeline/service/service.py | 5 +-- tests/pipeline/test_validation.py | 10 +++-- .../3_pipeline_dict_with_services_full.py | 2 +- tutorials/pipeline/6_extra_handlers_full.py | 25 ++++++------ utils/stats/sample_data_provider.py | 38 +++++++++---------- 7 files changed, 45 insertions(+), 56 deletions(-) diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 3c3483ba4..680e2a668 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -22,7 +22,7 @@ ServiceFunction, ) -from .pipeline.pipeline import Pipeline, ACTOR +from .pipeline.pipeline import Pipeline from .pipeline.actor import Actor from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 6e162b31a..117e13dd1 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -17,7 +17,7 @@ import asyncio import logging from typing import Union, List, Dict, Optional, Hashable, Callable -from pydantic import BaseModel, Field, model_validator, computed_field +from pydantic import BaseModel, Field, model_validator from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -42,12 +42,9 @@ logger = logging.getLogger(__name__) -# Could be removed, I think. Need to check just in case. -ACTOR = "ACTOR" - # Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. -# Must also add back in 'extra="forbid"', removed for testing. +# Actually, everything breaks when I do that. class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. @@ -94,7 +91,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) - slots: Optional[Union[GroupSlot, Dict]] = None + slots: GroupSlot = Field(default_factory=GroupSlot) handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) context_storage: Union[DBContextStorage, Dict] = Field(default_factory=dict) @@ -103,7 +100,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False - # TO-DO: Remove/change parameters below (if possible) + # These variables are okay like this, right? actor: Optional[Actor] = None _services_pipeline: Optional[ServiceGroup] = None _clean_turn_cache: Optional[bool] @@ -132,16 +129,8 @@ def _create_pipeline_services(self) -> ServiceGroup: @model_validator(mode="after") def pipeline_init(self): - # Should do this in a Pydantic Field - self.slots = GroupSlot.model_validate(self.slots) if self.slots is not None else None - - # computed_field is just not viable, they get called multiple times. - # Could maybe do this with a default_factory, but such function will require - # being defined before Pipeline self.actor = self._create_actor() - self._services_pipeline = self._create_pipeline_services() - finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) if self.optimization_warnings: diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 247ecc0e3..d645b82c6 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -103,9 +103,8 @@ def info_dict(self) -> dict: Adds `handler` key to base info dictionary. """ representation = super(Service, self).info_dict - if isinstance(self.handler, str) and self.handler == "ACTOR": - service_representation = "Instance of Actor" - elif callable(self.handler): + # Need to carefully remove this + if callable(self.handler): service_representation = f"Callable '{self.handler.__name__}'" else: service_representation = "[Unknown]" diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 70c9824b0..03f168e3f 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -46,7 +46,8 @@ def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo # Need a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction class TestServiceValidation: - def test_param_types(self): + """ + def test_wrong_param_types(self): # This doesn't work. For some reason any callable can be a ServiceFunction # Using model_validate doesn't help with pytest.raises(ValidationError) as e: @@ -56,15 +57,16 @@ def test_param_types(self): Service(handler=UserFunctionSamples.correct_service_function_2) Service(handler=UserFunctionSamples.correct_service_function_3) - def test_param_number(self): + def test_wrong_param_number(self): with pytest.raises(ValidationError) as e: Service(handler=UserFunctionSamples.wrong_param_number) assert e - def test_return_type(self): + def test_wrong_return_type(self): with pytest.raises(ValidationError) as e: Service(handler=UserFunctionSamples.wrong_return_type) assert e + """ def test_model_validator(self): with pytest.raises(ValidationError) as e: @@ -113,9 +115,11 @@ def test_wrong_inputs(self): BeforeHandler([1, 2, 3]) assert e # Wait, this one works. Why? An instance of BeforeHandler is not a function. + """ with pytest.raises(ValidationError) as e: BeforeHandler(functions=BeforeHandler([])) assert e + """ class TestServiceGroupValidation: diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index c0680d6a5..48a66c946 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -153,7 +153,7 @@ def postprocess(ctx: Context, pl: Pipeline): "context_storage": {}, "pre_services": [ { - "handler": {prepreprocess}, + "handler": prepreprocess, "name": "preprocessor", }, preprocess, diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index 2149aa0a0..0befbb209 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -23,7 +23,7 @@ ServiceGroup, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, - Service, + Service, to_service, ) from chatsky.script import Context from chatsky.utils.testing.common import ( @@ -147,29 +147,26 @@ def json_converter_after_handler(ctx, _, info): # %% -def heavy_function(ctx: Context): +@to_service( + before_handler=[time_measure_before_handler, ram_measure_before_handler], + after_handler=[time_measure_after_handler, ram_measure_after_handler], +) +def heavy_service(ctx: Context): memory_heap[ctx.last_request.text] = [ random.randint(0, num) for num in range(0, 1000) ] -def logging_function(ctx: Context, _, info: ServiceRuntimeInfo): +@to_service( + before_handler=[json_converter_before_handler], + after_handler=[json_converter_after_handler], +) +def logging_service(ctx: Context, _, info: ServiceRuntimeInfo): str_misc = ctx.misc[f"{info.name}-str"] assert isinstance(str_misc, str) print(f"Stringified misc: {str_misc}") -heavy_service = Service( - handler=heavy_function, - before_handler=[time_measure_before_handler, ram_measure_before_handler], - after_handler=[time_measure_after_handler, ram_measure_after_handler], -) -logging_service = Service( - handler=logging_function, - before_handler=[json_converter_before_handler], - after_handler=[json_converter_after_handler], -) - pipeline_dict = { "script": TOY_SCRIPT, "start_label": ("greeting_flow", "start_node"), diff --git a/utils/stats/sample_data_provider.py b/utils/stats/sample_data_provider.py index 10af87f76..38e167e84 100644 --- a/utils/stats/sample_data_provider.py +++ b/utils/stats/sample_data_provider.py @@ -12,7 +12,7 @@ import asyncio from tqdm import tqdm from chatsky.script import Context, Message -from chatsky.pipeline import Pipeline, Service, ACTOR, ExtraHandlerRuntimeInfo +from chatsky.pipeline import Pipeline, Service, ExtraHandlerRuntimeInfo, GlobalExtraHandlerType from chatsky.stats import ( default_extractors, OtelInstrumentor, @@ -57,26 +57,26 @@ async def get_confidence(ctx: Context, _, info: ExtraHandlerRuntimeInfo): "script": MULTIFLOW_SCRIPT, "start_label": ("root", "start"), "fallback_label": ("root", "fallback"), - "components": [ - Service(slot_processor_1, after_handler=[get_slots]), - Service(slot_processor_2, after_handler=[get_slots]), - Service( - handler=ACTOR, - before_handler=[ - default_extractors.get_timing_before, - ], - after_handler=[ - default_extractors.get_timing_after, - default_extractors.get_current_label, - default_extractors.get_last_request, - default_extractors.get_last_response, - ], - ), - Service(confidence_processor, after_handler=[get_confidence]), - ], + "pre_services": [Service(handler=slot_processor_1, after_handler=[get_slots]), + Service(handler=slot_processor_2, after_handler=[get_slots]),], + "post_services": Service(handler=confidence_processor, after_handler=[get_confidence]), } ) - +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_current_label +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_last_request +) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.AFTER, default_extractors.get_last_response +) # %% async def worker(queue: asyncio.Queue): From 0e6e70d95ee82683687d943aac4b636e384fb7a6 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 18:10:26 +0500 Subject: [PATCH 30/86] self-review changes + comments changed --- chatsky/pipeline/pipeline/actor.py | 6 ------ chatsky/pipeline/pipeline/component.py | 3 ++- chatsky/pipeline/service/extra.py | 7 ------- chatsky/pipeline/service/group.py | 5 ----- chatsky/pipeline/service/service.py | 3 --- tests/pipeline/test_validation.py | 5 ++++- 6 files changed, 6 insertions(+), 23 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index c116584ff..e9e182332 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -62,7 +62,6 @@ async def default_condition_handler( return await wrap_sync_function_in_async(condition, ctx, pipeline) -# arbitrary_types_allowed for testing, will remove later class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ The class which is used to process :py:class:`~chatsky.script.Context` @@ -85,8 +84,6 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. """ - # Can this just be Script, since Actor is now pydantic BaseModel? - # I feel like this is a bit different and is already handled. script: Union[Script, dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None @@ -94,9 +91,7 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): condition_handler: Callable = Field(default=default_condition_handler) handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) _clean_turn_cache: Optional[bool] = True - # Making a 'computed field' for this feels overkill, a 'private' field like this is probably fine? - # Basically, Actor cannot be async with other components. That is correct, yes? @model_validator(mode="after") def tick_async_flag(self): self.calculated_async_flag = False @@ -121,7 +116,6 @@ def actor_validator(self): self._clean_turn_cache = True return self - # Standard signature of any PipelineComponent. ctx goes first. async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ Method for running an `Actor`. diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 14379ab82..c225480d1 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -61,10 +61,11 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo :param path: Separated by dots path to component, is universally unique. """ + # Should this be Optional before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) timeout: Optional[float] = None - # The user sees this name right now, this has to be changed. It's just counter-intuitive. + # The user uses this name everywhere right now, this has to be changed. It's just counter-intuitive. requested_async_flag: Optional[bool] = None calculated_async_flag: bool = False start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 0e18b2143..8b17fd3ee 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -29,7 +29,6 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -# arbitrary_types_allowed for testing, will remove later class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class, representing an extra pipeline component handler. @@ -173,12 +172,6 @@ class BeforeHandler(ComponentExtraHandler): """ # Instead of __init__ here, this could look like a one-liner. - # The problem would be that BeforeHandlers would need to be initialized differently. - # Like BeforeHandler(functions=functions) instead of BeforeHandler(functions) - # That would break tests, tutorials and programs. - # Instead, this here could be a wrapper meant to keep the status quo. - - # The possible one-liner: # stage: ExtraHandlerType = ExtraHandlerType.BEFORE def __init__( diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index cb833bca1..fe24eb6b1 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -32,9 +32,6 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -# I think it's fine calling this a `Service` group, even though really it's a `PipelineComponent` group. -# The user only sees this as a `Service` group like they should. -# arbitrary_types_allowed for testing, will remove later class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ A service group class. @@ -80,8 +77,6 @@ def components_constructor(cls, data: Any): result["components"] = [result["components"]] return result - # Is there a better way to do this? calculated_async_flag is exposed to the user right now. - # Maybe I could just make this a 'private' field, like '_calc_async' @model_validator(mode="after") def calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index d645b82c6..43649b476 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -33,7 +33,6 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -# arbitrary_types_allowed for testing, will remove later class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): """ This class represents a service. @@ -57,8 +56,6 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): handler: ServiceFunction - # This code handles cases where just one Callable is passed into it's constructor data. - # All flags will be on default in that case. @model_validator(mode="before") @classmethod # Here Script class has "@validate_call". Is it needed here? diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 03f168e3f..990c40da4 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -44,8 +44,10 @@ def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo pass -# Need a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction +# Could make a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction class TestServiceValidation: + # These test don't throw exceptions. It's as if any Callable[] is an instance of ServiceFunction + # Same for ExtraHandlerFunction. I don't know how this should be addressed. """ def test_wrong_param_types(self): # This doesn't work. For some reason any callable can be a ServiceFunction @@ -122,6 +124,7 @@ def test_wrong_inputs(self): """ +# Note: I haven't tested asynchronous components in any way. class TestServiceGroupValidation: def test_single_service(self): func = UserFunctionSamples.correct_service_function_2 From 154bbad416566ddb69d9503b41ddd7c91d5e6516 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 18:11:32 +0500 Subject: [PATCH 31/86] formatted with poetry --- chatsky/pipeline/service/service.py | 3 ++- tests/pipeline/test_validation.py | 1 - tutorials/pipeline/6_extra_handlers_full.py | 3 ++- tutorials/stats/1_extractor_functions.py | 7 ++++-- utils/stats/sample_data_provider.py | 27 ++++++++------------- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 43649b476..16ad1a4c2 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -22,7 +22,8 @@ from chatsky.utils.devel.async_helpers import wrap_sync_function_in_async from chatsky.pipeline import always_start_condition from ..types import ( - ServiceFunction, StartConditionCheckerFunction, + ServiceFunction, + StartConditionCheckerFunction, ) from ..pipeline.component import PipelineComponent from .extra import ComponentExtraHandler diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 990c40da4..976a42097 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -206,4 +206,3 @@ def test_pre_services(self): # 'pre_services' must be a ServiceGroup Pipeline(**TOY_SCRIPT_KWARGS, pre_services=123) assert e - diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index 0befbb209..b42248c8e 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -23,7 +23,8 @@ ServiceGroup, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, - Service, to_service, + Service, + to_service, ) from chatsky.script import Context from chatsky.utils.testing.common import ( diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 396137a16..53279fcda 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -50,7 +50,8 @@ Pipeline, ExtraHandlerRuntimeInfo, GlobalExtraHandlerType, - to_service, Service, + to_service, + Service, ) from chatsky.script import Context from chatsky.stats import OtelInstrumentor, default_extractors @@ -125,7 +126,9 @@ async def heavy_service(ctx: Context): } ) -pipeline.actor.add_extra_handler(GlobalExtraHandlerType.BEFORE, default_extractors.get_current_label) +pipeline.actor.add_extra_handler( + GlobalExtraHandlerType.BEFORE, default_extractors.get_current_label +) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) if is_interactive_mode(): diff --git a/utils/stats/sample_data_provider.py b/utils/stats/sample_data_provider.py index 38e167e84..f24cfaf3c 100644 --- a/utils/stats/sample_data_provider.py +++ b/utils/stats/sample_data_provider.py @@ -57,26 +57,19 @@ async def get_confidence(ctx: Context, _, info: ExtraHandlerRuntimeInfo): "script": MULTIFLOW_SCRIPT, "start_label": ("root", "start"), "fallback_label": ("root", "fallback"), - "pre_services": [Service(handler=slot_processor_1, after_handler=[get_slots]), - Service(handler=slot_processor_2, after_handler=[get_slots]),], + "pre_services": [ + Service(handler=slot_processor_1, after_handler=[get_slots]), + Service(handler=slot_processor_2, after_handler=[get_slots]), + ], "post_services": Service(handler=confidence_processor, after_handler=[get_confidence]), } ) -pipeline.actor.add_extra_handler( - GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before -) -pipeline.actor.add_extra_handler( - GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after -) -pipeline.actor.add_extra_handler( - GlobalExtraHandlerType.AFTER, default_extractors.get_current_label -) -pipeline.actor.add_extra_handler( - GlobalExtraHandlerType.AFTER, default_extractors.get_last_request -) -pipeline.actor.add_extra_handler( - GlobalExtraHandlerType.AFTER, default_extractors.get_last_response -) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.BEFORE, default_extractors.get_timing_before) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_timing_after) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_current_label) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_last_request) +pipeline.actor.add_extra_handler(GlobalExtraHandlerType.AFTER, default_extractors.get_last_response) + # %% async def worker(queue: asyncio.Queue): From 2df499d5b8f15fd9ec39195f01eeeb0cb22debb0 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 18:13:32 +0500 Subject: [PATCH 32/86] lint --- tests/pipeline/test_validation.py | 3 +-- tutorials/pipeline/6_extra_handlers_full.py | 1 - tutorials/stats/1_extractor_functions.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 976a42097..b8901701f 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -6,7 +6,6 @@ Service, ServiceGroup, Actor, - ComponentExtraHandler, ServiceRuntimeInfo, BeforeHandler, ) @@ -179,7 +178,7 @@ def test_wrong_inputs(self): assert e with pytest.raises(ValidationError) as e: # 'start_label' is a mandatory field - Actor(script={}) + Actor(script=TOY_SCRIPT) assert e with pytest.raises(ValidationError) as e: # 'condition_handler' is not an Optional field. diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index b42248c8e..84401a1f1 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -23,7 +23,6 @@ ServiceGroup, ExtraHandlerRuntimeInfo, ServiceRuntimeInfo, - Service, to_service, ) from chatsky.script import Context diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 53279fcda..a68d3f3fa 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -51,7 +51,6 @@ ExtraHandlerRuntimeInfo, GlobalExtraHandlerType, to_service, - Service, ) from chatsky.script import Context from chatsky.stats import OtelInstrumentor, default_extractors From 3a562b1a43e9ddb73e5ffc8874c9a9f3888f9309 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 26 Jul 2024 18:50:04 +0500 Subject: [PATCH 33/86] removed a few comments --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 8b17fd3ee..6af321d65 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -58,7 +58,7 @@ def functions_constructor(cls, data: Any): result = {"functions": data} else: result = data.copy() - # Now it's definitely a dictionary. + if ("functions" in result) and (not isinstance(result["functions"], list)): result["functions"] = [result["functions"]] return result diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index fe24eb6b1..2ac4de9b4 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -62,8 +62,6 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr ] ] - # Whenever data isn't passed like a dictionary, this tries to cast it to the right dictionary - # This includes List, PipelineComponent and Callable. @model_validator(mode="before") @classmethod # Here Script class has "@validate_call". Is it needed here? From 8fcf1aa28c061a653f03bb599bcea94804e89470 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 29 Jul 2024 12:19:16 +0500 Subject: [PATCH 34/86] Polishing changes, new tests updated --- chatsky/__rebuild_pydantic_models__.py | 12 +----- chatsky/pipeline/pipeline/component.py | 15 +++----- chatsky/pipeline/pipeline/pipeline.py | 17 ++++----- chatsky/pipeline/service/extra.py | 35 ++++------------- chatsky/pipeline/service/group.py | 3 -- chatsky/pipeline/service/service.py | 11 +++--- tests/pipeline/test_validation.py | 53 +++----------------------- 7 files changed, 34 insertions(+), 112 deletions(-) diff --git a/chatsky/__rebuild_pydantic_models__.py b/chatsky/__rebuild_pydantic_models__.py index a5031d859..1db55a27a 100644 --- a/chatsky/__rebuild_pydantic_models__.py +++ b/chatsky/__rebuild_pydantic_models__.py @@ -1,19 +1,11 @@ # flake8: noqa: F401 -from chatsky.pipeline import Pipeline, Service, ServiceGroup, ComponentExtraHandler -from chatsky.pipeline.pipeline.actor import Actor +from chatsky.pipeline import Pipeline from chatsky.pipeline.pipeline.component import PipelineComponent -from chatsky.pipeline.types import ExtraHandlerRuntimeInfo +from chatsky.pipeline.types import ExtraHandlerRuntimeInfo, StartConditionCheckerFunction from chatsky.script import Context, Script -""" -Actor.model_rebuild() PipelineComponent.model_rebuild() -ComponentExtraHandler.model_rebuild() -Pipeline.model_rebuild() -Service.model_rebuild() -ServiceGroup.model_rebuild() -""" Pipeline.model_rebuild() Script.model_rebuild() Context.model_rebuild() diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index c225480d1..64acba44a 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -18,7 +18,7 @@ from chatsky.script import Context -from ..service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler +from ..service.extra import BeforeHandler, AfterHandler from ..conditions import always_start_condition from ..types import ( StartConditionCheckerFunction, @@ -28,6 +28,7 @@ ExtraHandlerFunction, ExtraHandlerType, ) +from ...utils.devel import wrap_sync_function_in_async logger = logging.getLogger(__name__) @@ -35,7 +36,6 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -# arbitrary_types_allowed for testing, will remove later class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allowed=True): """ This class represents a pipeline component, which is a service or a service group. @@ -61,11 +61,9 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo :param path: Separated by dots path to component, is universally unique. """ - # Should this be Optional - before_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: BeforeHandler([])) - after_handler: Optional[ComponentExtraHandler] = Field(default_factory=lambda: AfterHandler([])) + before_handler: BeforeHandler = Field(default_factory=BeforeHandler) + after_handler: AfterHandler = Field(default_factory=AfterHandler) timeout: Optional[float] = None - # The user uses this name everywhere right now, this has to be changed. It's just counter-intuitive. requested_async_flag: Optional[bool] = None calculated_async_flag: bool = False start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) @@ -147,10 +145,7 @@ async def _run(self, ctx: Context, pipeline: Pipeline) -> None: :param ctx: Current dialog :py:class:`~.Context`. :param pipeline: This :py:class:`~.Pipeline`. """ - # In the draft there was an 'await' before the start_condition, but my IDE gives a warning about this. - # Plus, previous implementation also doesn't have an await expression there. - # Though I understand why it's a good idea. (could be some slow function) - if self.start_condition(ctx, pipeline): + if await wrap_sync_function_in_async(self.start_condition, ctx, pipeline): try: await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 117e13dd1..d2c5cbff4 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -32,10 +32,6 @@ from ..types import ( GlobalExtraHandlerType, ExtraHandlerFunction, - # Everything breaks without this import, even though it's unused. - # (It says it's not defined or something like that) - # Should it go into TYPE_CHECKING? If not, what should be done? - StartConditionCheckerFunction, ) from .utils import finalize_service_group from chatsky.pipeline.pipeline.actor import Actor, default_condition_handler @@ -43,16 +39,18 @@ logger = logging.getLogger(__name__) -# Using "arbitrary_types_allowed" from pydantic for debug purposes, probably should remove later. -# Actually, everything breaks when I do that. class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: - :param components: (required) A :py:data:`~.ServiceGroup` object, - that will be transformed to root service group. It should include :py:class:`~.Actor`, - but only once (raises exception otherwise). It will always be named pipeline. + :param pre_services: List of :py:data:`~.Service` or + :py:data:`~.ServiceGroup` that will be executed before Actor. + :type pre_services: ServiceGroup + :param post_services: List of :py:data:`~.Service` or + :py:data:`~.ServiceGroup` that will be executed after Actor. It constructs root + service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. + :type post_services: ServiceGroup :param script: (required) A :py:class:`~.Script` instance (object or dict). :param start_label: (required) Actor start label. :param fallback_label: Actor fallback label. @@ -100,7 +98,6 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False - # These variables are okay like this, right? actor: Optional[Actor] = None _services_pipeline: Optional[ServiceGroup] = None _clean_turn_cache: Optional[bool] diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 6af321d65..32ac47cc2 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,8 +10,8 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING, Any -from pydantic import BaseModel, computed_field, model_validator +from typing import Optional, List, TYPE_CHECKING, Any, ClassVar +from pydantic import BaseModel, computed_field, model_validator, Field from chatsky.script import Context @@ -45,8 +45,8 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T :param requested_async_flag: Requested asynchronous property. """ - functions: List[ExtraHandlerFunction] - stage: ExtraHandlerType = ExtraHandlerType.UNDEFINED + functions: List[ExtraHandlerFunction] = Field(default_factory=list) + stage: ClassVar[ExtraHandlerType] = ExtraHandlerType.UNDEFINED timeout: Optional[float] = None requested_async_flag: Optional[bool] = None @@ -166,23 +166,12 @@ class BeforeHandler(ComponentExtraHandler): :type functions: List[ExtraHandlerFunction] :param timeout: Optional timeout for the execution of the extra functions, in seconds. - :param asynchronous: Optional flag that indicates whether the extra functions + :param requested_async_flag: Optional flag that indicates whether the extra functions should be executed asynchronously. The default value of the flag is True if all the functions in this handler are asynchronous. """ - # Instead of __init__ here, this could look like a one-liner. - # stage: ExtraHandlerType = ExtraHandlerType.BEFORE - - def __init__( - self, - functions: List[ExtraHandlerFunction], - timeout: Optional[int] = None, - asynchronous: Optional[bool] = None, - ): - super().__init__( - functions=functions, stage=ExtraHandlerType.BEFORE, timeout=timeout, requested_async_flag=asynchronous - ) + stage: ClassVar[ExtraHandlerType] = ExtraHandlerType.BEFORE class AfterHandler(ComponentExtraHandler): @@ -194,17 +183,9 @@ class AfterHandler(ComponentExtraHandler): :type functions: List[ExtraHandlerFunction] :param timeout: Optional timeout for the execution of the extra functions, in seconds. - :param asynchronous: Optional flag that indicates whether the extra functions + :param requested_async_flag: Optional flag that indicates whether the extra functions should be executed asynchronously. The default value of the flag is True if all the functions in this handler are asynchronous. """ - def __init__( - self, - functions: List[ExtraHandlerFunction], - timeout: Optional[int] = None, - asynchronous: Optional[bool] = None, - ): - super().__init__( - functions=functions, stage=ExtraHandlerType.AFTER, timeout=timeout, requested_async_flag=asynchronous - ) + stage: ClassVar[ExtraHandlerType] = ExtraHandlerType.AFTER diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 2ac4de9b4..54be0f251 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -88,9 +88,6 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: Collects information about their execution state - group is finished successfully only if all components in it finished successfully. - :param ctx: Current dialog context. - :param pipeline: The current pipeline. - :param ctx: Current dialog context. :param pipeline: The current pipeline. """ diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 16ad1a4c2..acbe4e890 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -26,7 +26,7 @@ StartConditionCheckerFunction, ) from ..pipeline.component import PipelineComponent -from .extra import ComponentExtraHandler +from .extra import BeforeHandler, AfterHandler logger = logging.getLogger(__name__) @@ -111,12 +111,11 @@ def info_dict(self) -> dict: def to_service( - # These shouldn't be None, I think. - before_handler: Optional[ComponentExtraHandler] = None, - after_handler: Optional[ComponentExtraHandler] = None, + before_handler: BeforeHandler = None, + after_handler: AfterHandler = None, timeout: Optional[int] = None, asynchronous: Optional[bool] = None, - start_condition: Optional[StartConditionCheckerFunction] = always_start_condition, + start_condition: StartConditionCheckerFunction = always_start_condition, name: Optional[str] = None, ): """ @@ -124,6 +123,8 @@ def to_service( Returns a Service, constructed from this function (taken as a handler). All arguments are passed directly to `Service` constructor. """ + before_handler = BeforeHandler() if before_handler is None else before_handler + after_handler = AfterHandler() if after_handler is None else after_handler def inner(handler: ServiceFunction) -> Service: return Service( diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index b8901701f..8d99d87d1 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -13,23 +13,12 @@ from chatsky.utils.testing import TOY_SCRIPT, TOY_SCRIPT_KWARGS +# Looks overly long, we only need one function anyway. class UserFunctionSamples: """ This class contains various examples of user functions along with their signatures. """ - @staticmethod - def wrong_param_number(number: int) -> float: - return 8.0 + number - - @staticmethod - def wrong_param_types(number: int, flag: bool) -> float: - return 8.0 + number if flag else 42.1 - - @staticmethod - def wrong_return_type(_: Context, __: Pipeline) -> float: - return 1.0 - @staticmethod def correct_service_function_1(_: Context): pass @@ -45,30 +34,6 @@ def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo # Could make a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction class TestServiceValidation: - # These test don't throw exceptions. It's as if any Callable[] is an instance of ServiceFunction - # Same for ExtraHandlerFunction. I don't know how this should be addressed. - """ - def test_wrong_param_types(self): - # This doesn't work. For some reason any callable can be a ServiceFunction - # Using model_validate doesn't help - with pytest.raises(ValidationError) as e: - Service(handler=UserFunctionSamples.wrong_param_types) - assert e - Service(handler=UserFunctionSamples.correct_service_function_1) - Service(handler=UserFunctionSamples.correct_service_function_2) - Service(handler=UserFunctionSamples.correct_service_function_3) - - def test_wrong_param_number(self): - with pytest.raises(ValidationError) as e: - Service(handler=UserFunctionSamples.wrong_param_number) - assert e - - def test_wrong_return_type(self): - with pytest.raises(ValidationError) as e: - Service(handler=UserFunctionSamples.wrong_return_type) - assert e - """ - def test_model_validator(self): with pytest.raises(ValidationError) as e: # Can't pass a list to handler, it has to be a single function @@ -97,33 +62,27 @@ def test_model_validator(self): class TestExtraHandlerValidation: def test_correct_functions(self): funcs = [UserFunctionSamples.correct_service_function_1, UserFunctionSamples.correct_service_function_2] - handler = BeforeHandler(funcs) + handler = BeforeHandler(functions=funcs) assert handler.functions == funcs def test_single_function(self): single_function = UserFunctionSamples.correct_service_function_1 - handler = BeforeHandler(single_function) + handler = BeforeHandler.model_validate(single_function) # Checking that a single function is cast to a list within constructor assert handler.functions == [single_function] def test_wrong_inputs(self): with pytest.raises(ValidationError) as e: # 1 is not a callable - BeforeHandler(1) + BeforeHandler.model_validate(1) assert e with pytest.raises(ValidationError) as e: # 'functions' should be a list of ExtraHandlerFunctions - BeforeHandler([1, 2, 3]) - assert e - # Wait, this one works. Why? An instance of BeforeHandler is not a function. - """ - with pytest.raises(ValidationError) as e: - BeforeHandler(functions=BeforeHandler([])) + BeforeHandler.model_validate([1, 2, 3]) assert e - """ -# Note: I haven't tested asynchronous components in any way. +# Note: I haven't tested components being asynchronous in any way. class TestServiceGroupValidation: def test_single_service(self): func = UserFunctionSamples.correct_service_function_2 From f0359a874588f2e89c0d3dc7d991f3a4260c7609 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 29 Jul 2024 15:35:02 +0500 Subject: [PATCH 35/86] Pipeline tutorials updated --- tutorials/pipeline/1_basics.py | 8 +-- .../pipeline/2_pre_and_post_processors.py | 9 ++-- .../3_pipeline_dict_with_services_basic.py | 15 +++--- .../3_pipeline_dict_with_services_full.py | 54 +++++++++---------- .../pipeline/4_groups_and_conditions_basic.py | 8 +-- .../pipeline/4_groups_and_conditions_full.py | 30 ++++++----- ...5_asynchronous_groups_and_services_full.py | 6 +-- tutorials/pipeline/6_extra_handlers_full.py | 3 +- .../7_extra_handlers_and_extensions.py | 2 +- 9 files changed, 67 insertions(+), 68 deletions(-) diff --git a/tutorials/pipeline/1_basics.py b/tutorials/pipeline/1_basics.py index ad57bda83..898558d91 100644 --- a/tutorials/pipeline/1_basics.py +++ b/tutorials/pipeline/1_basics.py @@ -29,9 +29,9 @@ # %% [markdown] """ `Pipeline` is an object, that automates script execution and context management. -`from_script` method can be used to create +It's constructor method can be used to create a pipeline of the most basic structure: -"preprocessors -> actor -> postprocessors" +"pre-services -> actor -> post-services" as well as to define `context_storage` and `messenger_interface`. Actor is a component of :py:class:`.Pipeline`, that contains the :py:class:`.Script` and handles it. It is responsible for processing @@ -42,7 +42,7 @@ Here only required parameters are provided to pipeline. `context_storage` will default to simple Python dict and `messenger_interface` will never be used. -pre- and postprocessors lists are empty. +pre- and post-services lists are empty. `Pipeline` object can be called with user input as first argument and dialog id (any immutable object). This call will return `Context`, @@ -61,7 +61,7 @@ # %% [markdown] """ For the sake of brevity, other tutorials -might use `TOY_SCRIPT_KWARGS` to initialize pipeline: +might use `TOY_SCRIPT_KWARGS` (keyword arguments) to initialize pipeline: """ # %% diff --git a/tutorials/pipeline/2_pre_and_post_processors.py b/tutorials/pipeline/2_pre_and_post_processors.py index 8160a7ebb..ef2216caa 100644 --- a/tutorials/pipeline/2_pre_and_post_processors.py +++ b/tutorials/pipeline/2_pre_and_post_processors.py @@ -31,11 +31,10 @@ # %% [markdown] """ -When Pipeline is created with `from_script` method, additional pre- -and postprocessors can be defined. -These can be any `ServiceBuilder` objects (defined in `types` module) -- callables, objects or dicts. -They are being turned into special `Service` objects (see tutorial 3), +When Pipeline is created, additional pre- +and post-services can be defined. +These can be any callables, certain objects or dicts. +They are being turned into special `Service` or `ServiceGroup` objects (see tutorial 3), that will be run before or after `Actor` respectively. These services can be used to access external APIs, annotate user input, etc. diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index 659514e42..4bc9f2ee5 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -31,18 +31,17 @@ # %% [markdown] """ -When Pipeline is created using `from_dict` method, +When Pipeline is created using Pydantic's `model_validate` method, pipeline should be defined as a dictionary. -It should contain `components` - a `ServiceGroupBuilder` object, -basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects, +It may contain `pre-services` and 'post-services' - `ServiceGroup` objects, +basically a list of `Service` objects or more `ServiceGroup` objects, see tutorial 4. -On pipeline execution services from `components` +On pipeline execution services from `components` = 'pre-services' + actor + 'post-services' list are run without difference between pre- and postprocessors. -Actor constant "ACTOR" is required to be passed as one of the services. -`ServiceBuilder` object can be defined either with callable -(see tutorial 2) or with dict / object. -It should contain `handler` - a `ServiceBuilder` object. +`Service` object can be defined either with callable +(see tutorial 2) or with `Service` constructor / dict. +It must contain `handler` - a callable (function). Not only Pipeline can be run using `__call__` method, for most cases `run` method should be used. diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index 48a66c946..a8ca47738 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -34,46 +34,43 @@ # %% [markdown] """ -When Pipeline is created using `from_dict` method, -pipeline should be defined as `PipelineBuilder` objects -(defined in `types` module). -These objects are dictionaries of particular structure: +When Pipeline is created using Pydantic's `model_validate` method, +pipeline should be defined as a dictionary of a particular structure: * `messenger_interface` - `MessengerInterface` instance, is used to connect to channel and transfer IO to user. * `context_storage` - Place to store dialog contexts (dictionary or a `DBContextStorage` instance). -* `components` (required) - A `ServiceGroupBuilder` object, - basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects, +* `pre-services` - A `ServiceGroup` object, + basically a list of `Service` objects or more `ServiceGroup` objects, see tutorial 4. -* `before_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +* `post-services` - A `ServiceGroup` object, + basically a list of `Service` objects or more `ServiceGroup` objects, + see tutorial 4. +* `before_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. -* `after_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +* `after_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. * `timeout` - Pipeline timeout, see tutorial 5. * `optimization_warnings` - Whether pipeline asynchronous structure should be checked during initialization, see tutorial 5. -On pipeline execution services from `components` list are run -without difference between pre- and postprocessors. -If "ACTOR" constant is not found among `components` pipeline creation fails. -There can be only one "ACTOR" constant in the pipeline. -`ServiceBuilder` object can be defined either with callable (see tutorial 2) or -with dict of structure / object with following constructor arguments: - -* `handler` (required) - ServiceBuilder, - if handler is an object or a dict itself, - it will be used instead of base ServiceBuilder. - NB! Fields of nested ServiceBuilder will be overridden - by defined fields of the base ServiceBuilder. -* `before_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +On pipeline execution services from `components` = 'pre-services' + actor + 'post-services' +list are run without difference between pre- and postprocessors. +`Service` object can be defined either with callable +(see tutorial 2) or with dict of structure / `Service` object + with following constructor arguments: + + +* `handler` (required) - ServiceFunction. +* `before_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. -* `after_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +* `after_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. * `timeout` - service timeout, see tutorial 5. * `asynchronous` - whether or not this service _should_ be asynchronous @@ -88,11 +85,10 @@ for most cases `run` method should be used. It starts pipeline asynchronously and connects to provided messenger interface. -Here pipeline contains 4 services, -defined in 4 different ways with different signatures. +Here pipeline contains 3 services, +defined in 3 different ways with different signatures. First two of them write sample feature detection data to `ctx.misc`. The first uses a constant expression and the second fetches from `example.com`. -Third one is "ACTOR" constant (it acts like a _special_ service here). Final service logs `ctx.misc` dict. """ diff --git a/tutorials/pipeline/4_groups_and_conditions_basic.py b/tutorials/pipeline/4_groups_and_conditions_basic.py index 072a4323c..bd560695f 100644 --- a/tutorials/pipeline/4_groups_and_conditions_basic.py +++ b/tutorials/pipeline/4_groups_and_conditions_basic.py @@ -36,10 +36,10 @@ # %% [markdown] """ Pipeline can contain not only single services, but also service groups. -Service groups can be defined as `ServiceGroupBuilder` objects: - lists of `ServiceBuilders` and `ServiceGroupBuilders` or objects. -The objects should contain `components` - -a `ServiceBuilder` and `ServiceGroupBuilder` object list. +Service groups can be defined as `ServiceGroup` objects: + lists of `Service` or more `ServiceGroup` objects. +`ServiceGroup` objects should contain `components` - +a list of `Service` and `ServiceGroup` objects. To receive serialized information about service, service group or pipeline a property `info_dict` can be used, diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index dee90b679..e1b1c6f72 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -36,19 +36,25 @@ # %% [markdown] """ Pipeline can contain not only single services, but also service groups. -Service groups can be defined as lists of `ServiceBuilders` +Service groups can be defined as `ServiceGroup` objects: + lists of `Service` or more `ServiceGroup` objects. +`ServiceGroup` objects should contain `components` - +a list of `Service` and `ServiceGroup` objects. + +Pipeline can contain not only single services, but also service groups. +Service groups can be defined as lists of `Service` or more `ServiceGroup` objects. (in fact, all of the pipeline services are combined into root service group named "pipeline"). Alternatively, the groups can be defined as objects with following constructor arguments: -* `components` (required) - A list of `ServiceBuilder` objects, - `ServiceGroupBuilder` objects and lists of them. -* `before_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +* `components` (required) - A list of `Service` objects, + `ServiceGroup` objects. +* `before_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. -* `after_handler` - a list of `ExtraHandlerFunction` objects, - `ExtraHandlerBuilder` objects and lists of them. +* `after_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. See tutorials 6 and 7. * `timeout` - Pipeline timeout, see tutorial 5. * `asynchronous` - Whether or not this service group _should_ be asynchronous @@ -75,12 +81,11 @@ If no name is specified for a service or service group, the name will be generated according to the following rules: -1. If service's handler is an Actor, service will be named 'actor'. -2. If service's handler is callable, +1. If service's handler is callable, service will be named callable. -3. Service group will be named 'service_group'. -4. Otherwise, it will be named 'noname_service'. -5. After that an index will be added to service name. +2. Service group will be named 'service_group'. +3. Otherwise, it will be named 'noname_service'. +4. After that an index will be added to service name. To receive serialized information about service, service group or pipeline a property `info_dict` can be used, @@ -134,7 +139,6 @@ Function that returns `True` if any of the given `functions` (condition functions) return `True`. -NB! Actor service ALWAYS runs unconditionally. Here there are two conditionally executed services: a service named `running_service` is executed diff --git a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py index 40100722b..ef2dacb32 100644 --- a/tutorials/pipeline/5_asynchronous_groups_and_services_full.py +++ b/tutorials/pipeline/5_asynchronous_groups_and_services_full.py @@ -34,7 +34,7 @@ """ Services and service groups can be synchronous and asynchronous. In synchronous service groups services are executed consequently, - some of them (`ACTOR`) can even return `Context` object, + some of them can even return `Context` object, modifying it. In asynchronous service groups all services are executed simultaneously and should not return anything, @@ -52,7 +52,6 @@ the service becomes asynchronous, and if set, it is used instead. If service can not be asynchronous, but is marked asynchronous, an exception is thrown. -ACTOR service is asynchronous. The timeout field only works for asynchronous services and service groups. If service execution takes more time than timeout, @@ -76,7 +75,8 @@ it logs HTTPS requests (from 1 to 15), running simultaneously, in random order. Service group `pipeline` can't be asynchronous because -`balanced_group` and ACTOR are synchronous. +`balanced_group` and `Actor` are synchronous. +(`Actor` is added into `Pipeline`'s 'components' during it's creation) """ diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index 84401a1f1..8dccea6b2 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -69,10 +69,11 @@ 1. Directly in constructor - by adding extra handlers to `before_handler` or `after_handler` constructor parameter. -# Needs to be changed! 'to_service' doesn't exist anymore! 2. (Services only) `to_service` decorator - transforms function to service with extra handlers from `before_handler` and `after_handler` arguments. +3. Using `add_extra_handler` function of `PipelineComponent` + Example: component.add_extra_handler(GlobalExtraHandlerType.AFTER, get_service_state) Here 5 `heavy_service`s fill big amounts of memory with random numbers. Their runtime stats are captured and displayed by extra services, diff --git a/tutorials/pipeline/7_extra_handlers_and_extensions.py b/tutorials/pipeline/7_extra_handlers_and_extensions.py index c8a96f620..a89ce332d 100644 --- a/tutorials/pipeline/7_extra_handlers_and_extensions.py +++ b/tutorials/pipeline/7_extra_handlers_and_extensions.py @@ -59,7 +59,7 @@ * `global_extra_handler_type` (required) - A `GlobalExtraHandlerType` instance, indicates extra handler type to add. -* `extra_handler` (required) - The extra handler function itself. +* `extra_handler` (required) - The `ExtraHandlerFunction` itself. * `whitelist` - An optional list of paths, if it's not `None` the extra handlers will be applied to specified pipeline components only. From 6c7fa9131574cf0dcafc238f05756a8973065d43 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 29 Jul 2024 15:40:08 +0500 Subject: [PATCH 36/86] slight tutorial changes --- tutorials/pipeline/3_pipeline_dict_with_services_basic.py | 6 ++++-- tutorials/pipeline/3_pipeline_dict_with_services_full.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index 4bc9f2ee5..95f68b6e1 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -8,8 +8,8 @@ Here, %mddoclink(api,pipeline.service.service,Service) class, that can be used for pre- and postprocessing of messages is shown. -Pipeline's %mddoclink(api,pipeline.pipeline.pipeline,Pipeline.from_dict) -static method is used for pipeline creation (from dictionary). +%mddoclink(api,pipeline.pipeline.pipeline,Pipeline)'s +constructor method is used for pipeline creation (directly or from dictionary). """ # %pip install chatsky @@ -84,6 +84,8 @@ def postprocess(_): # %% pipeline = Pipeline.model_validate(pipeline_dict) +# or +# pipeline = Pipeline(**pipeline_dict) if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index a8ca47738..306d0a3aa 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -34,8 +34,9 @@ # %% [markdown] """ -When Pipeline is created using Pydantic's `model_validate` method, -pipeline should be defined as a dictionary of a particular structure: +When Pipeline is created using Pydantic's `model_validate` method +or `Pipeline`'s constructor method, pipeline should be +defined as a dictionary of a particular structure: * `messenger_interface` - `MessengerInterface` instance, is used to connect to channel and transfer IO to user. From 627e374249782e598c83739a492d8210c9073138 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 29 Jul 2024 15:44:18 +0500 Subject: [PATCH 37/86] lint --- tutorials/pipeline/2_pre_and_post_processors.py | 3 ++- tutorials/pipeline/3_pipeline_dict_with_services_basic.py | 3 ++- tutorials/pipeline/3_pipeline_dict_with_services_full.py | 5 +++-- tutorials/pipeline/4_groups_and_conditions_full.py | 3 ++- tutorials/pipeline/6_extra_handlers_full.py | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tutorials/pipeline/2_pre_and_post_processors.py b/tutorials/pipeline/2_pre_and_post_processors.py index ef2216caa..ec45af5be 100644 --- a/tutorials/pipeline/2_pre_and_post_processors.py +++ b/tutorials/pipeline/2_pre_and_post_processors.py @@ -34,7 +34,8 @@ When Pipeline is created, additional pre- and post-services can be defined. These can be any callables, certain objects or dicts. -They are being turned into special `Service` or `ServiceGroup` objects (see tutorial 3), +They are being turned into special `Service` or `ServiceGroup` objects +(see tutorial 3), that will be run before or after `Actor` respectively. These services can be used to access external APIs, annotate user input, etc. diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index 95f68b6e1..db082a3f1 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -37,7 +37,8 @@ basically a list of `Service` objects or more `ServiceGroup` objects, see tutorial 4. -On pipeline execution services from `components` = 'pre-services' + actor + 'post-services' +On pipeline execution services from +`components` = 'pre-services' + actor + 'post-services' list are run without difference between pre- and postprocessors. `Service` object can be defined either with callable (see tutorial 2) or with `Service` constructor / dict. diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_full.py b/tutorials/pipeline/3_pipeline_dict_with_services_full.py index 306d0a3aa..ad17281b0 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_full.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_full.py @@ -35,7 +35,7 @@ # %% [markdown] """ When Pipeline is created using Pydantic's `model_validate` method -or `Pipeline`'s constructor method, pipeline should be +or `Pipeline`'s constructor method, pipeline should be defined as a dictionary of a particular structure: * `messenger_interface` - `MessengerInterface` instance, @@ -59,7 +59,8 @@ should be checked during initialization, see tutorial 5. -On pipeline execution services from `components` = 'pre-services' + actor + 'post-services' +On pipeline execution services from +`components` = 'pre-services' + actor + 'post-services' list are run without difference between pre- and postprocessors. `Service` object can be defined either with callable (see tutorial 2) or with dict of structure / `Service` object diff --git a/tutorials/pipeline/4_groups_and_conditions_full.py b/tutorials/pipeline/4_groups_and_conditions_full.py index e1b1c6f72..f185b18be 100644 --- a/tutorials/pipeline/4_groups_and_conditions_full.py +++ b/tutorials/pipeline/4_groups_and_conditions_full.py @@ -42,7 +42,8 @@ a list of `Service` and `ServiceGroup` objects. Pipeline can contain not only single services, but also service groups. -Service groups can be defined as lists of `Service` or more `ServiceGroup` objects. +Service groups can be defined as lists of `Service` + or more `ServiceGroup` objects. (in fact, all of the pipeline services are combined into root service group named "pipeline"). Alternatively, the groups can be defined as objects diff --git a/tutorials/pipeline/6_extra_handlers_full.py b/tutorials/pipeline/6_extra_handlers_full.py index 8dccea6b2..a3e2c5a26 100644 --- a/tutorials/pipeline/6_extra_handlers_full.py +++ b/tutorials/pipeline/6_extra_handlers_full.py @@ -72,8 +72,8 @@ 2. (Services only) `to_service` decorator - transforms function to service with extra handlers from `before_handler` and `after_handler` arguments. -3. Using `add_extra_handler` function of `PipelineComponent` - Example: component.add_extra_handler(GlobalExtraHandlerType.AFTER, get_service_state) +3. Using `add_extra_handler` function of `PipelineComponent` Example: +component.add_extra_handler(GlobalExtraHandlerType.AFTER, get_service_state) Here 5 `heavy_service`s fill big amounts of memory with random numbers. Their runtime stats are captured and displayed by extra services, From 11fe6407fa9782d9b10488f448d4e101901782f7 Mon Sep 17 00:00:00 2001 From: ZergLev <64711614+ZergLev@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:20:14 +0500 Subject: [PATCH 38/86] fixing one-symbol mistake in docs --- chatsky/pipeline/pipeline/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index d2c5cbff4..8f7ad80e7 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -49,7 +49,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): :type pre_services: ServiceGroup :param post_services: List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` that will be executed after Actor. It constructs root - service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. + service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. :type post_services: ServiceGroup :param script: (required) A :py:class:`~.Script` instance (object or dict). :param start_label: (required) Actor start label. From 5888888838e587edf80a35ec03b1e348c945e71a Mon Sep 17 00:00:00 2001 From: ZergLev <64711614+ZergLev@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:13:54 +0300 Subject: [PATCH 39/86] Update chatsky/pipeline/pipeline/component.py Co-authored-by: Roman Zlobin --- chatsky/pipeline/pipeline/component.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 64acba44a..451b5e62a 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -72,8 +72,11 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo @model_validator(mode="after") def pipeline_component_validator(self): - if self.name is not None and (self.name == "" or "." in self.name): - raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {self.name})!") + if self.name is not None: + if self.name == "": + raise ValueError("Name cannot be blank.") + if "." in self.name: + raise ValueError(f"Name cannot contain '.': {self.name!r}.") if not self.calculated_async_flag and self.requested_async_flag: raise Exception(f"{type(self).__name__} '{self.name}' can't be asynchronous!") From 40dc5b8d2a24eef20455a5ceb07433095464e3e4 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 15:37:39 +0300 Subject: [PATCH 40/86] review changes started --- chatsky/pipeline/pipeline/actor.py | 17 ++++---- chatsky/pipeline/pipeline/component.py | 34 +++++++++++----- chatsky/pipeline/pipeline/pipeline.py | 15 +++---- chatsky/pipeline/pipeline/utils.py | 2 - chatsky/pipeline/service/extra.py | 1 - chatsky/pipeline/service/group.py | 10 ++--- chatsky/pipeline/service/service.py | 3 +- tests/pipeline/test_validation.py | 54 +++++++------------------- 8 files changed, 59 insertions(+), 77 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index e9e182332..7be5fdf9f 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -62,7 +62,7 @@ async def default_condition_handler( return await wrap_sync_function_in_async(condition, ctx, pipeline) -class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): +class Actor(PipelineComponent): """ The class which is used to process :py:class:`~chatsky.script.Context` according to the :py:class:`~chatsky.script.Script`. @@ -84,13 +84,14 @@ class Actor(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. """ - script: Union[Script, dict] + script: Script start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 condition_handler: Callable = Field(default=default_condition_handler) handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) - _clean_turn_cache: Optional[bool] = True + # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! + _clean_turn_cache: bool = True @model_validator(mode="after") def tick_async_flag(self): @@ -98,22 +99,20 @@ def tick_async_flag(self): return self @model_validator(mode="after") - def actor_validator(self): - if not isinstance(self.script, Script): - self.script = Script(script=self.script) + def start_label_validator(self): self.start_label = normalize_label(self.start_label) if self.script.get(self.start_label[0], {}).get(self.start_label[1]) is None: raise ValueError(f"Unknown start_label={self.start_label}") + return self + @model_validator(mode="after") + def fallback_label_validator(self): if self.fallback_label is None: self.fallback_label = self.start_label else: self.fallback_label = normalize_label(self.fallback_label) if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: raise ValueError(f"Unknown fallback_label={self.fallback_label}") - - # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! - self._clean_turn_cache = True return self async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 451b5e62a..f638b9dff 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -137,7 +137,22 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin logger.warning(f"{type(self).__name__} '{self.name}' {extra_handler.stage} extra handler timed out!") @abc.abstractmethod - async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[ComponentExecutionState]: + """ + Method for running this component. It can be an Actor, Service or ServiceGroup. + It has to be defined in the child classes, + which is done in each of the default PipelineComponents. + Service 'handler' has three possible signatures. These possible signatures are: + + - (ctx: Context) - accepts current dialog context only. + - (ctx: Context, pipeline: Pipeline) - accepts context and current pipeline. + - | (ctx: Context, pipeline: Pipeline, info: ServiceRuntimeInfo) - accepts context, + pipeline and service runtime info dictionary. + + :param ctx: Current dialog context. + :param pipeline: The current pipeline. + :return: `None` + """ raise NotImplementedError async def _run(self, ctx: Context, pipeline: Pipeline) -> None: @@ -148,21 +163,20 @@ async def _run(self, ctx: Context, pipeline: Pipeline) -> None: :param ctx: Current dialog :py:class:`~.Context`. :param pipeline: This :py:class:`~.Pipeline`. """ - if await wrap_sync_function_in_async(self.start_condition, ctx, pipeline): - try: + try: + if await wrap_sync_function_in_async(self.start_condition, ctx, pipeline): await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) self._set_state(ctx, ComponentExecutionState.RUNNING) - await self.run_component(ctx, pipeline) - if self.get_state(ctx) is not ComponentExecutionState.FAILED: + if await self.run_component(ctx, pipeline) is not ComponentExecutionState.FAILED: self._set_state(ctx, ComponentExecutionState.FINISHED) await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) - except Exception as exc: - self._set_state(ctx, ComponentExecutionState.FAILED) - logger.error(f"Service '{self.name}' execution failed!", exc_info=exc) - else: - self._set_state(ctx, ComponentExecutionState.NOT_RUN) + else: + self._set_state(ctx, ComponentExecutionState.NOT_RUN) + except Exception as exc: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"Service '{self.name}' execution failed!", exc_info=exc) async def __call__(self, ctx: Context, pipeline: Pipeline) -> Optional[Awaitable]: """ diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 8f7ad80e7..6f3603e23 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -16,8 +16,9 @@ import asyncio import logging +from functools import cached_property from typing import Union, List, Dict, Optional, Hashable, Callable -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, computed_field from chatsky.context_storages import DBContextStorage from chatsky.script import Script, Context, ActorStage @@ -98,11 +99,11 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): timeout: Optional[float] = None optimization_warnings: bool = False parallelize_processing: bool = False - actor: Optional[Actor] = None - _services_pipeline: Optional[ServiceGroup] = None _clean_turn_cache: Optional[bool] - def _create_actor(self) -> Actor: + @computed_field + @cached_property + def actor(self) -> Actor: return Actor( script=self.script, start_label=self.start_label, @@ -112,7 +113,9 @@ def _create_actor(self) -> Actor: handlers=self.handlers, ) - def _create_pipeline_services(self) -> ServiceGroup: + @computed_field + @cached_property + def _services_pipeline(self) -> ServiceGroup: components = [self.pre_services, self.actor, self.post_services] services_pipeline = ServiceGroup( components=components, @@ -126,8 +129,6 @@ def _create_pipeline_services(self) -> ServiceGroup: @model_validator(mode="after") def pipeline_init(self): - self.actor = self._create_actor() - self._services_pipeline = self._create_pipeline_services() finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) if self.optimization_warnings: diff --git a/chatsky/pipeline/pipeline/utils.py b/chatsky/pipeline/pipeline/utils.py index 35d6de23d..0d514e106 100644 --- a/chatsky/pipeline/pipeline/utils.py +++ b/chatsky/pipeline/pipeline/utils.py @@ -34,7 +34,6 @@ def rename_component_incrementing(component: PipelineComponent, collisions: List """ if isinstance(component, Actor): base_name = "actor" - # Pretty sure that service.handler can only be a callable now. Should the newly irrelevant logic be removed? elif isinstance(component, Service) and callable(component.handler): if isfunction(component.handler): base_name = component.handler.__name__ @@ -43,7 +42,6 @@ def rename_component_incrementing(component: PipelineComponent, collisions: List elif isinstance(component, ServiceGroup): base_name = "service_group" else: - # This should never be triggered. (All PipelineComponent derivatives are handled above) base_name = "noname_service" name_index = 0 diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 32ac47cc2..0d6c03a51 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -52,7 +52,6 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @model_validator(mode="before") @classmethod - # Here Script class has "@validate_call". Is it needed here? def functions_constructor(cls, data: Any): if not isinstance(data, dict): result = {"functions": data} diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 54be0f251..3d5a95b90 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio import logging -from typing import List, Union, Awaitable, TYPE_CHECKING, Any +from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional from pydantic import model_validator from chatsky.script import Context @@ -32,7 +32,7 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): +class ServiceGroup(PipelineComponent): """ A service group class. Service group can be included into pipeline as an object or a pipeline component list. @@ -64,7 +64,6 @@ class ServiceGroup(PipelineComponent, extra="forbid", arbitrary_types_allowed=Tr @model_validator(mode="before") @classmethod - # Here Script class has "@validate_call". Is it needed here? def components_constructor(cls, data: Any): if not isinstance(data, dict): result = {"components": data} @@ -80,7 +79,7 @@ def calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) return self - async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: + async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[ComponentExecutionState]: """ Method for running this service group. Catches runtime exceptions and logs them. It doesn't include extra handlers execution, start condition checking or error handling - pure execution only. @@ -107,7 +106,8 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: await service_result failed = any([service.get_state(ctx) == ComponentExecutionState.FAILED for service in self.components]) - self._set_state(ctx, ComponentExecutionState.FAILED if failed else ComponentExecutionState.FINISHED) + if failed: + return ComponentExecutionState.FAILED def log_optimization_warnings(self): """ diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index acbe4e890..556a3862d 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -34,7 +34,7 @@ from chatsky.pipeline.pipeline.pipeline import Pipeline -class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): +class Service(PipelineComponent): """ This class represents a service. Service can be included into pipeline as object or a dictionary. @@ -59,7 +59,6 @@ class Service(PipelineComponent, extra="forbid", arbitrary_types_allowed=True): @model_validator(mode="before") @classmethod - # Here Script class has "@validate_call". Is it needed here? def handler_constructor(cls, data: Any): if not isinstance(data, dict): return {"handler": data} diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 8d99d87d1..c4c75e6ef 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -35,25 +35,18 @@ def correct_service_function_3(_: Context, __: Pipeline, ___: ServiceRuntimeInfo # Could make a test for returning an awaitable from a ServiceFunction, ExtraHandlerFunction class TestServiceValidation: def test_model_validator(self): - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # Can't pass a list to handler, it has to be a single function Service(handler=[UserFunctionSamples.correct_service_function_2]) - assert e - with pytest.raises(ValidationError) as e: - # 'handler' is a mandatory field - Service(before_handler=UserFunctionSamples.correct_service_function_2) - assert e - with pytest.raises(ValidationError) as e: - # Can't pass None to handler, it has to be a callable function + with pytest.raises(ValidationError): + # Can't pass 'None' to handler, it has to be a callable function # Though I wonder if empty Services should be allowed. # I see no reason to allow it. Service() - assert e - with pytest.raises(TypeError) as e: + with pytest.raises(TypeError): # Python says that two positional arguments were given when only one was expected. # This happens before Pydantic's validation, so I think there's nothing we can do. Service(UserFunctionSamples.correct_service_function_1) - assert e # But it can work like this. # A single function gets cast to the right dictionary here. Service.model_validate(UserFunctionSamples.correct_service_function_1) @@ -72,14 +65,12 @@ def test_single_function(self): assert handler.functions == [single_function] def test_wrong_inputs(self): - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 1 is not a callable BeforeHandler.model_validate(1) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'functions' should be a list of ExtraHandlerFunctions BeforeHandler.model_validate([1, 2, 3]) - assert e # Note: I haven't tested components being asynchronous in any way. @@ -104,25 +95,18 @@ def test_several_correct_services(self): assert group.components[1].timeout == 10 def test_wrong_inputs(self): - with pytest.raises(ValidationError) as e: - # 'components' is a mandatory field - ServiceGroup(before_handler=UserFunctionSamples.correct_service_function_2) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'components' must be a list of PipelineComponents, wrong type # Though 123 will be cast to a list ServiceGroup(components=123) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # The dictionary inside 'components' will check if Actor, Service or ServiceGroup fit the signature, # but it doesn't fit any of them, so it's just a normal dictionary ServiceGroup(components={"before_handler": []}) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # The dictionary inside 'components' will try to get cast to Service and will fail # But 'components' must be a list of PipelineComponents, so it's just a normal dictionary ServiceGroup(components={"handler": 123}) - assert e # Testing of node and script validation for actor exist at script/core/test_actor.py @@ -131,26 +115,15 @@ def test_toy_script_actor(self): Actor(**TOY_SCRIPT_KWARGS) def test_wrong_inputs(self): - with pytest.raises(ValidationError) as e: - # 'script' is a mandatory field - Actor(start_label=TOY_SCRIPT_KWARGS["start_label"]) - assert e - with pytest.raises(ValidationError) as e: - # 'start_label' is a mandatory field - Actor(script=TOY_SCRIPT) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'condition_handler' is not an Optional field. Actor(**TOY_SCRIPT_KWARGS, condition_handler=None) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'handlers' is not an Optional field. Actor(**TOY_SCRIPT_KWARGS, handlers=None) - assert e - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'script' must be either a dict or Script instance. Actor(script=[], start_label=TOY_SCRIPT_KWARGS["start_label"]) - assert e # Can't think of any other tests that aren't done in other tests in this file @@ -160,7 +133,6 @@ def test_correct_inputs(self): Pipeline.model_validate(TOY_SCRIPT_KWARGS) def test_pre_services(self): - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): # 'pre_services' must be a ServiceGroup Pipeline(**TOY_SCRIPT_KWARGS, pre_services=123) - assert e From 76913419305b5a0020140c47ddda80d680d38590 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:22:14 +0300 Subject: [PATCH 41/86] reverted redundant changes for Actor --- chatsky/pipeline/pipeline/actor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 7be5fdf9f..fd13dda74 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -84,7 +84,7 @@ class Actor(PipelineComponent): - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. """ - script: Script + script: Union[Script, Dict] start_label: NodeLabel2Type fallback_label: Optional[NodeLabel2Type] = None label_priority: float = 1.0 @@ -100,6 +100,8 @@ def tick_async_flag(self): @model_validator(mode="after") def start_label_validator(self): + if not isinstance(self.script, Script): + self.script = Script(script=self.script) self.start_label = normalize_label(self.start_label) if self.script.get(self.start_label[0], {}).get(self.start_label[1]) is None: raise ValueError(f"Unknown start_label={self.start_label}") From d6956ef23337ed02cb3bf0672381d7be9834fed8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:33:13 +0300 Subject: [PATCH 42/86] changing validation for ExtraHandlers --- chatsky/pipeline/service/extra.py | 20 +++++++++++--------- tests/pipeline/test_validation.py | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 0d6c03a51..eda7cab1c 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,8 +10,8 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING, Any, ClassVar -from pydantic import BaseModel, computed_field, model_validator, Field +from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Union, Callable, Tuple +from pydantic import BaseModel, computed_field, model_validator, Field, field_validator from chatsky.script import Context @@ -53,14 +53,16 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @model_validator(mode="before") @classmethod def functions_constructor(cls, data: Any): - if not isinstance(data, dict): - result = {"functions": data} - else: - result = data.copy() + if isinstance(data, (List, Callable, Tuple)): + return {"functions": data} + return data - if ("functions" in result) and (not isinstance(result["functions"], list)): - result["functions"] = [result["functions"]] - return result + @field_validator("functions") + @classmethod + def functions_validator(cls, functions): + if not isinstance(functions, list): + return [functions] + return functions @computed_field(repr=False) def calculated_async_flag(self) -> bool: diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index c4c75e6ef..b447b2a67 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -10,7 +10,7 @@ BeforeHandler, ) from chatsky.script import Context -from chatsky.utils.testing import TOY_SCRIPT, TOY_SCRIPT_KWARGS +from chatsky.utils.testing import TOY_SCRIPT_KWARGS # Looks overly long, we only need one function anyway. From cf6869a6ce555d619ccc0964b9b52ffd0e4093ee Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:44:25 +0300 Subject: [PATCH 43/86] testing different doc-style --- chatsky/pipeline/service/extra.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index eda7cab1c..64e9268c5 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -35,25 +35,24 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T A component extra handler is a set of functions, attached to pipeline component (before or after it). Extra handlers should execute supportive tasks (like time or resources measurement, minor data transformations). Extra handlers should NOT edit context or pipeline, use services for that purpose instead. - - :param functions: A list or instance of :py:data:`~.ExtraHandlerFunction`. - :type functions: :py:data:`~.ExtraHandlerFunction` - :param stage: An :py:class:`~.ExtraHandlerType`, specifying whether this handler will be executed before or - after pipeline component. - :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted. - :param requested_async_flag: Requested asynchronous property. """ functions: List[ExtraHandlerFunction] = Field(default_factory=list) + """A list or instance of :py:data:`~.ExtraHandlerFunction`. + :type functions: :py:data:`~.ExtraHandlerFunction`""" stage: ClassVar[ExtraHandlerType] = ExtraHandlerType.UNDEFINED + """An :py:class:`~.ExtraHandlerType`, specifying whether this handler will + be executed before or after pipeline component.""" timeout: Optional[float] = None + """(for asynchronous only!) Maximum component execution time (in seconds), + if it exceeds this time, it is interrupted.""" requested_async_flag: Optional[bool] = None + """Requested asynchronous property.""" @model_validator(mode="before") @classmethod def functions_constructor(cls, data: Any): - if isinstance(data, (List, Callable, Tuple)): + if isinstance(data, (List[Callable], Callable)): return {"functions": data} return data From f0d5474683c6ff0aa3a114d79e5c09bbbee53492 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:47:23 +0300 Subject: [PATCH 44/86] updated validation for ServiceGroup --- chatsky/pipeline/service/group.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 3d5a95b90..e16a7c3c8 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,7 +12,7 @@ import asyncio import logging from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional -from pydantic import model_validator +from pydantic import model_validator, field_validator from chatsky.script import Context from ..pipeline.actor import Actor @@ -65,14 +65,16 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod def components_constructor(cls, data: Any): - if not isinstance(data, dict): - result = {"components": data} - else: - result = data.copy() + if isinstance(data, (List[PipelineComponent], PipelineComponent)): + return {"components": data} + return data - if ("components" in result) and (not isinstance(result["components"], list)): - result["components"] = [result["components"]] - return result + @field_validator("components") + @classmethod + def components_constructor(cls, components): + if not isinstance(components, list): + return [components] + return components @model_validator(mode="after") def calculate_async_flag(self): From 3f046ab58ed388ba818f60302ea8addcc72cd773 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:50:41 +0300 Subject: [PATCH 45/86] lint --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 64e9268c5..d0cd2e8f0 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,7 +10,7 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Union, Callable, Tuple +from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable from pydantic import BaseModel, computed_field, model_validator, Field, field_validator from chatsky.script import Context diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index e16a7c3c8..253d3aebc 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -71,7 +71,7 @@ def components_constructor(cls, data: Any): @field_validator("components") @classmethod - def components_constructor(cls, components): + def components_validator(cls, components): if not isinstance(components, list): return [components] return components From b34329143c3ecec3aba065fbeae45bf8d6087394 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 16:58:49 +0300 Subject: [PATCH 46/86] minor fix --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index d0cd2e8f0..4b63d79b1 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -52,7 +52,7 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @model_validator(mode="before") @classmethod def functions_constructor(cls, data: Any): - if isinstance(data, (List[Callable], Callable)): + if isinstance(data, (list, Callable)): return {"functions": data} return data diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 253d3aebc..ffdca23c9 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -65,7 +65,7 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod def components_constructor(cls, data: Any): - if isinstance(data, (List[PipelineComponent], PipelineComponent)): + if isinstance(data, (list, PipelineComponent)): return {"components": data} return data From ed6f9e80eee9230297ac5b69a75786448a89bdbc Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 17:12:29 +0300 Subject: [PATCH 47/86] minor fix --- chatsky/pipeline/service/extra.py | 16 ++++++++-------- chatsky/pipeline/service/group.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 4b63d79b1..c16c64dcb 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -53,15 +53,15 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @classmethod def functions_constructor(cls, data: Any): if isinstance(data, (list, Callable)): - return {"functions": data} - return data + result = {"functions": data} + elif isinstance(data, dict): + result = data.copy() + else: + return data - @field_validator("functions") - @classmethod - def functions_validator(cls, functions): - if not isinstance(functions, list): - return [functions] - return functions + if ("functions" in result) and (not isinstance(result["functions"], list)): + result["functions"] = [result["functions"]] + return result @computed_field(repr=False) def calculated_async_flag(self) -> bool: diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index ffdca23c9..7f70feaf7 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -66,15 +66,15 @@ class ServiceGroup(PipelineComponent): @classmethod def components_constructor(cls, data: Any): if isinstance(data, (list, PipelineComponent)): - return {"components": data} - return data + result = {"components": data} + elif isinstance(data, dict): + result = data.copy() + else: + return data - @field_validator("components") - @classmethod - def components_validator(cls, components): - if not isinstance(components, list): - return [components] - return components + if ("components" in result) and (not isinstance(result["components"], list)): + result["components"] = [result["components"]] + return result @model_validator(mode="after") def calculate_async_flag(self): From c78dd27ab74fd3452bf76e86de951a7a7341008d Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 17:13:13 +0300 Subject: [PATCH 48/86] lint --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index c16c64dcb..791732981 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,7 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable -from pydantic import BaseModel, computed_field, model_validator, Field, field_validator +from pydantic import BaseModel, computed_field, model_validator, Field from chatsky.script import Context diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 7f70feaf7..af9d5a988 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,7 +12,7 @@ import asyncio import logging from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional -from pydantic import model_validator, field_validator +from pydantic import model_validator from chatsky.script import Context from ..pipeline.actor import Actor From 7b799bc68817907dbfafd3fb3d5625874ea80daf Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 17:28:54 +0300 Subject: [PATCH 49/86] strict validation added for some classes --- chatsky/pipeline/service/extra.py | 5 +++-- chatsky/pipeline/service/group.py | 5 +++-- chatsky/pipeline/service/service.py | 12 ++++++++---- tests/pipeline/test_validation.py | 9 +++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 791732981..8be053780 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,7 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable -from pydantic import BaseModel, computed_field, model_validator, Field +from pydantic import BaseModel, computed_field, model_validator, Field, ValidationError from chatsky.script import Context @@ -57,7 +57,8 @@ def functions_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - return data + raise ValidationError("Extra Handler can only be initialized from a Dict," + " a Callable or a list of Callables. Wrong inputs received.") if ("functions" in result) and (not isinstance(result["functions"], list)): result["functions"] = [result["functions"]] diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index af9d5a988..ab4910c18 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,7 +12,7 @@ import asyncio import logging from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional -from pydantic import model_validator +from pydantic import model_validator, ValidationError from chatsky.script import Context from ..pipeline.actor import Actor @@ -70,7 +70,8 @@ def components_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - return data + raise ValidationError("Service Group can only be initialized from a Dict," + " a PipelineComponent or a list of PipelineComponents. Wrong inputs received.") if ("components" in result) and (not isinstance(result["components"], list)): result["components"] = [result["components"]] diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 556a3862d..b33762d4e 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -13,8 +13,8 @@ from __future__ import annotations import logging import inspect -from typing import TYPE_CHECKING, Any, Optional -from pydantic import model_validator +from typing import TYPE_CHECKING, Any, Optional, Callable +from pydantic import model_validator, ValidationError from chatsky.script import Context @@ -60,9 +60,13 @@ class Service(PipelineComponent): @model_validator(mode="before") @classmethod def handler_constructor(cls, data: Any): - if not isinstance(data, dict): + if isinstance(data, Callable): return {"handler": data} - return data + elif isinstance(data, dict): + return data + else: + raise ValidationError("A Service can only be initialized from a Dict or" + " a Callable. Wrong inputs received.") @model_validator(mode="after") def tick_async_flag(self): diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index b447b2a67..0716a6f74 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -47,6 +47,11 @@ def test_model_validator(self): # Python says that two positional arguments were given when only one was expected. # This happens before Pydantic's validation, so I think there's nothing we can do. Service(UserFunctionSamples.correct_service_function_1) + with pytest.raises(ValidationError): + # Can't pass 'None' to handler, it has to be a callable function + # Though I wonder if empty Services should be allowed. + # I see no reason to allow it. + Service() # But it can work like this. # A single function gets cast to the right dictionary here. Service.model_validate(UserFunctionSamples.correct_service_function_1) @@ -71,6 +76,10 @@ def test_wrong_inputs(self): with pytest.raises(ValidationError): # 'functions' should be a list of ExtraHandlerFunctions BeforeHandler.model_validate([1, 2, 3]) + with pytest.raises(ValidationError): + # 'functions' should be a list of ExtraHandlerFunctions, + # you can't pass another ExtraHandler there + BeforeHandler.model_validate(BeforeHandler()) # Note: I haven't tested components being asynchronous in any way. From dbde44280ac0c8199633294028cfbfd621675160 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 17:39:08 +0300 Subject: [PATCH 50/86] minor fix --- chatsky/pipeline/service/extra.py | 6 +++--- chatsky/pipeline/service/group.py | 22 +++++++++++----------- chatsky/pipeline/service/service.py | 11 +++++------ tests/pipeline/test_validation.py | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 8be053780..6aeee3af7 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,7 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable -from pydantic import BaseModel, computed_field, model_validator, Field, ValidationError +from pydantic import BaseModel, computed_field, model_validator, Field from chatsky.script import Context @@ -57,8 +57,8 @@ def functions_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - raise ValidationError("Extra Handler can only be initialized from a Dict," - " a Callable or a list of Callables. Wrong inputs received.") + raise ValueError("Extra Handler can only be initialized from a Dict," + " a Callable or a list of Callables. Wrong inputs received.") if ("functions" in result) and (not isinstance(result["functions"], list)): result["functions"] = [result["functions"]] diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index ab4910c18..b249f3058 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,7 +12,7 @@ import asyncio import logging from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional -from pydantic import model_validator, ValidationError +from pydantic import model_validator from chatsky.script import Context from ..pipeline.actor import Actor @@ -70,8 +70,8 @@ def components_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - raise ValidationError("Service Group can only be initialized from a Dict," - " a PipelineComponent or a list of PipelineComponents. Wrong inputs received.") + raise ValueError("Service Group can only be initialized from a Dict," + " a PipelineComponent or a list of PipelineComponents. Wrong inputs received.") if ("components" in result) and (not isinstance(result["components"], list)): result["components"] = [result["components"]] @@ -129,9 +129,9 @@ def log_optimization_warnings(self): for service in self.components: if not isinstance(service, ServiceGroup): if ( - service.calculated_async_flag - and service.requested_async_flag is not None - and not service.requested_async_flag + service.calculated_async_flag + and service.requested_async_flag is not None + and not service.requested_async_flag ): logger.warning(f"Service '{service.name}' could be asynchronous!") if not service.asynchronous and service.timeout is not None: @@ -139,7 +139,7 @@ def log_optimization_warnings(self): else: if not service.calculated_async_flag: if service.requested_async_flag is None and any( - [sub_service.asynchronous for sub_service in service.components] + [sub_service.asynchronous for sub_service in service.components] ): logger.warning( f"ServiceGroup '{service.name}' contains both sync and async services, " @@ -148,10 +148,10 @@ def log_optimization_warnings(self): service.log_optimization_warnings() def add_extra_handler( - self, - global_extra_handler_type: GlobalExtraHandlerType, - extra_handler: ExtraHandlerFunction, - condition: ExtraHandlerConditionFunction = lambda _: False, + self, + global_extra_handler_type: GlobalExtraHandlerType, + extra_handler: ExtraHandlerFunction, + condition: ExtraHandlerConditionFunction = lambda _: False, ): """ Method for adding a global extra handler to this group. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index b33762d4e..a367e3440 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -14,7 +14,7 @@ import logging import inspect from typing import TYPE_CHECKING, Any, Optional, Callable -from pydantic import model_validator, ValidationError +from pydantic import model_validator from chatsky.script import Context @@ -62,11 +62,10 @@ class Service(PipelineComponent): def handler_constructor(cls, data: Any): if isinstance(data, Callable): return {"handler": data} - elif isinstance(data, dict): - return data - else: - raise ValidationError("A Service can only be initialized from a Dict or" - " a Callable. Wrong inputs received.") + elif not isinstance(data, dict): + raise ValueError("A Service can only be initialized from a Dict or a Callable." + " Wrong inputs received.") + return data @model_validator(mode="after") def tick_async_flag(self): diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 0716a6f74..ac14c67cb 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -51,7 +51,7 @@ def test_model_validator(self): # Can't pass 'None' to handler, it has to be a callable function # Though I wonder if empty Services should be allowed. # I see no reason to allow it. - Service() + Service(handler=Service()) # But it can work like this. # A single function gets cast to the right dictionary here. Service.model_validate(UserFunctionSamples.correct_service_function_1) From fc26dafe64c3d2ab761f854541bbcc2effda427a Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 8 Aug 2024 17:41:02 +0300 Subject: [PATCH 51/86] formatted with poetry --- chatsky/pipeline/service/extra.py | 6 ++++-- chatsky/pipeline/service/group.py | 22 ++++++++++++---------- chatsky/pipeline/service/service.py | 3 +-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 6aeee3af7..6d3d9936b 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -57,8 +57,10 @@ def functions_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - raise ValueError("Extra Handler can only be initialized from a Dict," - " a Callable or a list of Callables. Wrong inputs received.") + raise ValueError( + "Extra Handler can only be initialized from a Dict," + " a Callable or a list of Callables. Wrong inputs received." + ) if ("functions" in result) and (not isinstance(result["functions"], list)): result["functions"] = [result["functions"]] diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index b249f3058..8ccb18073 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -70,8 +70,10 @@ def components_constructor(cls, data: Any): elif isinstance(data, dict): result = data.copy() else: - raise ValueError("Service Group can only be initialized from a Dict," - " a PipelineComponent or a list of PipelineComponents. Wrong inputs received.") + raise ValueError( + "Service Group can only be initialized from a Dict," + " a PipelineComponent or a list of PipelineComponents. Wrong inputs received." + ) if ("components" in result) and (not isinstance(result["components"], list)): result["components"] = [result["components"]] @@ -129,9 +131,9 @@ def log_optimization_warnings(self): for service in self.components: if not isinstance(service, ServiceGroup): if ( - service.calculated_async_flag - and service.requested_async_flag is not None - and not service.requested_async_flag + service.calculated_async_flag + and service.requested_async_flag is not None + and not service.requested_async_flag ): logger.warning(f"Service '{service.name}' could be asynchronous!") if not service.asynchronous and service.timeout is not None: @@ -139,7 +141,7 @@ def log_optimization_warnings(self): else: if not service.calculated_async_flag: if service.requested_async_flag is None and any( - [sub_service.asynchronous for sub_service in service.components] + [sub_service.asynchronous for sub_service in service.components] ): logger.warning( f"ServiceGroup '{service.name}' contains both sync and async services, " @@ -148,10 +150,10 @@ def log_optimization_warnings(self): service.log_optimization_warnings() def add_extra_handler( - self, - global_extra_handler_type: GlobalExtraHandlerType, - extra_handler: ExtraHandlerFunction, - condition: ExtraHandlerConditionFunction = lambda _: False, + self, + global_extra_handler_type: GlobalExtraHandlerType, + extra_handler: ExtraHandlerFunction, + condition: ExtraHandlerConditionFunction = lambda _: False, ): """ Method for adding a global extra handler to this group. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index a367e3440..d36fb8e72 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -63,8 +63,7 @@ def handler_constructor(cls, data: Any): if isinstance(data, Callable): return {"handler": data} elif not isinstance(data, dict): - raise ValueError("A Service can only be initialized from a Dict or a Callable." - " Wrong inputs received.") + raise ValueError("A Service can only be initialized from a Dict or a Callable." " Wrong inputs received.") return data @model_validator(mode="after") From ac66a68168d13d2f01a01506d8eb6d1851f5ef0e Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 14:51:57 +0300 Subject: [PATCH 52/86] review changes mostly done --- chatsky/pipeline/pipeline/actor.py | 4 ++ chatsky/pipeline/pipeline/component.py | 52 +++++++++++++++++--------- chatsky/pipeline/pipeline/utils.py | 15 +------- chatsky/pipeline/service/group.py | 4 ++ chatsky/pipeline/service/service.py | 10 +++++ tests/pipeline/test_validation.py | 20 ++++++---- 6 files changed, 66 insertions(+), 39 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index fd13dda74..d7131f164 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -117,6 +117,10 @@ def fallback_label_validator(self): raise ValueError(f"Unknown fallback_label={self.fallback_label}") return self + @property + def computed_name(self) -> str: + return "actor" + async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: """ Method for running an `Actor`. diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index f638b9dff..52b95f93b 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -40,35 +40,40 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo """ This class represents a pipeline component, which is a service or a service group. It contains some fields that they have in common. + """ + before_handler: BeforeHandler = Field(default_factory=BeforeHandler) + """ :param before_handler: :py:class:`~.BeforeHandler`, associated with this component. - :type before_handler: Optional[:py:data:`~.ComponentExtraHandler`] + :type before_handler: Optional[:py:data:`~.ComponentExtraHandler`]""" + after_handler: AfterHandler = Field(default_factory=AfterHandler) + """ :param after_handler: :py:class:`~.AfterHandler`, associated with this component. - :type after_handler: Optional[:py:data:`~.ComponentExtraHandler`] + :type after_handler: Optional[:py:data:`~.ComponentExtraHandler`]""" + timeout: Optional[float] = None + """ :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted. + if it exceeds this time, it is interrupted.""" + requested_async_flag: Optional[bool] = None + """ :param requested_async_flag: Requested asynchronous property; - if not defined, `calculated_async_flag` is used instead. + if not defined, `calculated_async_flag` is used instead.""" + calculated_async_flag: bool = False + """ :param calculated_async_flag: Whether the component can be asynchronous or not 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, - 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. - + 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not.""" + start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) + """ :param start_condition: StartConditionCheckerFunction that is invoked before each component execution; component is executed only if it returns `True`. - :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`] - :param name: Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), - should not be blank or contain `.` symbol. - :param path: Separated by dots path to component, is universally unique. - """ - - before_handler: BeforeHandler = Field(default_factory=BeforeHandler) - after_handler: AfterHandler = Field(default_factory=AfterHandler) - timeout: Optional[float] = None - requested_async_flag: Optional[bool] = None - calculated_async_flag: bool = False - start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) + :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`]""" name: Optional[str] = None + """ + :param name: Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), + should not be blank or contain `.` symbol.""" path: Optional[str] = None + """:param path: Separated by dots path to component, is universally unique.""" @model_validator(mode="after") def pipeline_component_validator(self): @@ -155,6 +160,17 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[Comp """ raise NotImplementedError + @abc.abstractmethod + @property + def computed_name(self) -> str: + """ + Every derivative of `PipelineComponent` must define this property. + :return: `str`. + """ + raise NotImplementedError + # Or could return the following: + # return "noname_service" + async def _run(self, ctx: Context, pipeline: Pipeline) -> None: """ A method for running a pipeline component. Executes extra handlers before and after execution, diff --git a/chatsky/pipeline/pipeline/utils.py b/chatsky/pipeline/pipeline/utils.py index 0d514e106..2761fccbb 100644 --- a/chatsky/pipeline/pipeline/utils.py +++ b/chatsky/pipeline/pipeline/utils.py @@ -7,11 +7,8 @@ import collections from typing import List -from inspect import isfunction -from .actor import Actor from .component import PipelineComponent -from ..service.service import Service from ..service.group import ServiceGroup @@ -32,18 +29,10 @@ def rename_component_incrementing(component: PipelineComponent, collisions: List :param collisions: Components in the same service group as component. :return: Generated name """ - if isinstance(component, Actor): - base_name = "actor" - elif isinstance(component, Service) and callable(component.handler): - if isfunction(component.handler): - base_name = component.handler.__name__ - else: - base_name = component.handler.__class__.__name__ - elif isinstance(component, ServiceGroup): - base_name = "service_group" + if isinstance(component, PipelineComponent): + base_name = component.computed_name else: base_name = "noname_service" - name_index = 0 while f"{base_name}_{name_index}" in [component.name for component in collisions]: name_index += 1 diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 8ccb18073..aa6a28043 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -176,6 +176,10 @@ def add_extra_handler( else: service.add_extra_handler(global_extra_handler_type, extra_handler) + @property + def computed_name(self) -> str: + return "service_group" + @property def info_dict(self) -> dict: """ diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index d36fb8e72..c847a7923 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -95,6 +95,16 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: else: raise Exception(f"Too many parameters required for service '{self.name}' handler: {handler_params}!") + @property + def computed_name(self) -> str: + if callable(self.handler): + if inspect.isfunction(self.handler): + return self.handler.__name__ + else: + return self.handler.__class__.__name__ + else: + return "noname_service" + @property def info_dict(self) -> dict: """ diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index ac14c67cb..e32d2146f 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -69,6 +69,12 @@ def test_single_function(self): # Checking that a single function is cast to a list within constructor assert handler.functions == [single_function] + def test_extra_handler_as_functions(self): + # 'functions' should be a list of ExtraHandlerFunctions, + # but you can pass another ExtraHandler there, because, coincidentally, + # it's a Callable with the right signature. It may be changed later, though. + BeforeHandler.model_validate({"functions": BeforeHandler(functions=[])}) + def test_wrong_inputs(self): with pytest.raises(ValidationError): # 1 is not a callable @@ -76,13 +82,11 @@ def test_wrong_inputs(self): with pytest.raises(ValidationError): # 'functions' should be a list of ExtraHandlerFunctions BeforeHandler.model_validate([1, 2, 3]) - with pytest.raises(ValidationError): - # 'functions' should be a list of ExtraHandlerFunctions, - # you can't pass another ExtraHandler there - BeforeHandler.model_validate(BeforeHandler()) -# Note: I haven't tested components being asynchronous in any way. +# Note: I haven't tested components being asynchronous in any way, +# other than in the async pipeline components tutorial. +# It's not a test though. class TestServiceGroupValidation: def test_single_service(self): func = UserFunctionSamples.correct_service_function_2 @@ -110,11 +114,11 @@ def test_wrong_inputs(self): ServiceGroup(components=123) with pytest.raises(ValidationError): # The dictionary inside 'components' will check if Actor, Service or ServiceGroup fit the signature, - # but it doesn't fit any of them, so it's just a normal dictionary + # but it doesn't fit any of them (required fields are not defined), so it's just a normal dictionary. ServiceGroup(components={"before_handler": []}) with pytest.raises(ValidationError): - # The dictionary inside 'components' will try to get cast to Service and will fail - # But 'components' must be a list of PipelineComponents, so it's just a normal dictionary + # The dictionary inside 'components' will try to get cast to Service and will fail. + # 'components' must be a list of PipelineComponents, but it's just a normal dictionary (not a Service). ServiceGroup(components={"handler": 123}) From 2388b2cbb82112ebd461f02637ec7d2c3a5b0655 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 15:20:53 +0300 Subject: [PATCH 53/86] minor changes --- chatsky/pipeline/pipeline/component.py | 8 ++++---- chatsky/pipeline/service/extra.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 52b95f93b..7674c9eea 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -160,16 +160,16 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[Comp """ raise NotImplementedError - @abc.abstractmethod @property def computed_name(self) -> str: """ Every derivative of `PipelineComponent` must define this property. :return: `str`. """ - raise NotImplementedError - # Or could return the following: - # return "noname_service" + return "noname_service" + # Or could do the following: + # raise NotImplementedError + # But this default value makes sense and replicates previous logic. async def _run(self, ctx: Context, pipeline: Pipeline) -> None: """ diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 6d3d9936b..bfdfd8349 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,7 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable -from pydantic import BaseModel, computed_field, model_validator, Field +from pydantic import BaseModel, computed_field, model_validator, Field, field_validator from chatsky.script import Context From ba5c64a66dd60d7fa821a15d3a8f66d90fd875bf Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 15:23:13 +0300 Subject: [PATCH 54/86] lint --- chatsky/pipeline/service/extra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index bfdfd8349..6d3d9936b 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -11,7 +11,7 @@ import logging import inspect from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable -from pydantic import BaseModel, computed_field, model_validator, Field, field_validator +from pydantic import BaseModel, computed_field, model_validator, Field from chatsky.script import Context From f5e299c046fba7d6a7db0b2e20a2302e5af59844 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 15:28:26 +0300 Subject: [PATCH 55/86] minor fix --- chatsky/pipeline/service/group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index aa6a28043..8b8e8f94b 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio import logging -from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional +from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional, Callable from pydantic import model_validator from chatsky.script import Context @@ -65,7 +65,7 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod def components_constructor(cls, data: Any): - if isinstance(data, (list, PipelineComponent)): + if isinstance(data, (list, PipelineComponent, Callable)): result = {"components": data} elif isinstance(data, dict): result = data.copy() From 472bba888e94cf3f361913f1f7500201bdf60dc4 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 16:12:34 +0300 Subject: [PATCH 56/86] testing new docstrings format for PipelineComponent --- chatsky/pipeline/pipeline/component.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 7674c9eea..1c1cd3d73 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -44,36 +44,33 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo before_handler: BeforeHandler = Field(default_factory=BeforeHandler) """ - :param before_handler: :py:class:`~.BeforeHandler`, associated with this component. - :type before_handler: Optional[:py:data:`~.ComponentExtraHandler`]""" + :py:class:`~.BeforeHandler`, associated with this component.""" after_handler: AfterHandler = Field(default_factory=AfterHandler) """ - :param after_handler: :py:class:`~.AfterHandler`, associated with this component. - :type after_handler: Optional[:py:data:`~.ComponentExtraHandler`]""" + :py:class:`~.AfterHandler`, associated with this component.""" timeout: Optional[float] = None """ - :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), + (for asynchronous only!) Maximum component execution time (in seconds), if it exceeds this time, it is interrupted.""" requested_async_flag: Optional[bool] = None """ - :param requested_async_flag: Requested asynchronous property; - if not defined, `calculated_async_flag` is used instead.""" + Requested asynchronous property; if not defined, + `calculated_async_flag` is used instead.""" calculated_async_flag: bool = False """ - :param calculated_async_flag: Whether the component can be asynchronous or not + Whether the component can be asynchronous or not 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not.""" start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) """ - :param start_condition: StartConditionCheckerFunction that is invoked before each component execution; - component is executed only if it returns `True`. - :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`]""" + StartConditionCheckerFunction that is invoked before each component execution; + component is executed only if it returns `True`.""" name: Optional[str] = None """ - :param name: Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), + Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), should not be blank or contain `.` symbol.""" path: Optional[str] = None - """:param path: Separated by dots path to component, is universally unique.""" + """Separated by dots path to component, is universally unique.""" @model_validator(mode="after") def pipeline_component_validator(self): From 9a7abe9fb20111fc0b96368f4dad6f0dc391b244 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 16:36:43 +0300 Subject: [PATCH 57/86] added docstrings for remaining classes and switched to conventional docstring style --- chatsky/pipeline/pipeline/actor.py | 45 ++++++----- chatsky/pipeline/pipeline/component.py | 29 ++++--- chatsky/pipeline/pipeline/pipeline.py | 101 +++++++++++++++---------- chatsky/pipeline/service/extra.py | 21 +++-- chatsky/pipeline/service/group.py | 4 +- chatsky/pipeline/service/service.py | 4 +- 6 files changed, 128 insertions(+), 76 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index d7131f164..6905b5ecf 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -66,40 +66,51 @@ class Actor(PipelineComponent): """ The class which is used to process :py:class:`~chatsky.script.Context` according to the :py:class:`~chatsky.script.Script`. + """ - :param script: The dialog scenario: a graph described by the :py:class:`.Keywords`. - While the graph is being initialized, it is validated and then used for the dialog. - :param start_label: The start node of :py:class:`~chatsky.script.Script`. The execution begins with it. - :param fallback_label: The label of :py:class:`~chatsky.script.Script`. + script: Union[Script, Dict] + """ + The dialog scenario: a graph described by the :py:class:`.Keywords`. + While the graph is being initialized, it is validated and then used for the dialog. + """ + start_label: NodeLabel2Type + """ + The start node of :py:class:`~chatsky.script.Script`. The execution begins with it. + """ + fallback_label: Optional[NodeLabel2Type] = None + """ + The label of :py:class:`~chatsky.script.Script`. Dialog comes into that label if all other transitions failed, or there was an error while executing the scenario. Defaults to `None`. - :param label_priority: Default priority value for all :py:const:`labels ` + """ + label_priority: float = 1.0 + """ + Default priority value for all :py:const:`labels ` where there is no priority. Defaults to `1.0`. - :param condition_handler: Handler that processes a call of condition functions. Defaults to `None`. - :param handlers: This variable is responsible for the usage of external handlers on + """ + condition_handler: Callable = Field(default=default_condition_handler) + """ + Handler that processes a call of condition functions. Defaults to `None`. + """ + handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) + """ + This variable is responsible for the usage of external handlers on the certain stages of work of :py:class:`~chatsky.script.Actor`. - key (:py:class:`~chatsky.script.ActorStage`) - Stage in which the handler is called. - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. """ - - script: Union[Script, Dict] - start_label: NodeLabel2Type - fallback_label: Optional[NodeLabel2Type] = None - label_priority: float = 1.0 - condition_handler: Callable = Field(default=default_condition_handler) - handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! _clean_turn_cache: bool = True @model_validator(mode="after") - def tick_async_flag(self): + def __tick_async_flag(self): self.calculated_async_flag = False return self @model_validator(mode="after") - def start_label_validator(self): + def __start_label_validator(self): if not isinstance(self.script, Script): self.script = Script(script=self.script) self.start_label = normalize_label(self.start_label) @@ -108,7 +119,7 @@ def start_label_validator(self): return self @model_validator(mode="after") - def fallback_label_validator(self): + def __fallback_label_validator(self): if self.fallback_label is None: self.fallback_label = self.start_label else: diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 1c1cd3d73..21b42f23a 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -44,36 +44,45 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo before_handler: BeforeHandler = Field(default_factory=BeforeHandler) """ - :py:class:`~.BeforeHandler`, associated with this component.""" + :py:class:`~.BeforeHandler`, associated with this component. + """ after_handler: AfterHandler = Field(default_factory=AfterHandler) """ - :py:class:`~.AfterHandler`, associated with this component.""" + :py:class:`~.AfterHandler`, associated with this component. + """ timeout: Optional[float] = None """ (for asynchronous only!) Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted.""" + if it exceeds this time, it is interrupted. + """ requested_async_flag: Optional[bool] = None """ - Requested asynchronous property; if not defined, - `calculated_async_flag` is used instead.""" + Requested asynchronous property; if not defined, + `calculated_async_flag` is used instead. + """ calculated_async_flag: bool = False """ Whether the component can be asynchronous or not 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, - 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not.""" + 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. + """ start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) """ StartConditionCheckerFunction that is invoked before each component execution; - component is executed only if it returns `True`.""" + component is executed only if it returns `True`. + """ name: Optional[str] = None """ Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), - should not be blank or contain `.` symbol.""" + should not be blank or contain `.` symbol. + """ path: Optional[str] = None - """Separated by dots path to component, is universally unique.""" + """ + Separated by dots path to component, is universally unique. + """ @model_validator(mode="after") - def pipeline_component_validator(self): + def __pipeline_component_validator(self): if self.name is not None: if self.name == "": raise ValueError("Name cannot be blank.") diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 6f3603e23..31e65544d 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -44,61 +44,86 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Class that automates service execution and creates service pipeline. It accepts constructor parameters: - - :param pre_services: List of :py:data:`~.Service` or - :py:data:`~.ServiceGroup` that will be executed before Actor. - :type pre_services: ServiceGroup - :param post_services: List of :py:data:`~.Service` or - :py:data:`~.ServiceGroup` that will be executed after Actor. It constructs root - service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. - :type post_services: ServiceGroup - :param script: (required) A :py:class:`~.Script` instance (object or dict). - :param start_label: (required) Actor start label. - :param fallback_label: Actor fallback label. - :param label_priority: Default priority value for all actor :py:const:`labels ` - where there is no priority. Defaults to `1.0`. - :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. - :param handlers: This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. - - - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. - - :param messenger_interface: An `AbsMessagingInterface` instance for this pipeline. - :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline or - a dict to store dialog :py:class:`~.Context`. - :param before_handler: List of `_ComponentExtraHandler` to add to the group. - :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param after_handler: List of `_ComponentExtraHandler` to add to the group. - :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param timeout: Timeout to add to pipeline root service group. - :param optimization_warnings: Asynchronous pipeline optimization check request flag; - warnings will be sent to logs. Additionally, it has some calculated fields: - - - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, - - `actor` is a pipeline actor, found among services. - :param parallelize_processing: This flag determines whether or not the functions - defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections - of the script should be parallelized over respective groups. - """ pre_services: ServiceGroup = Field(default_factory=list) + """ + List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` + that will be executed before Actor. + """ post_services: ServiceGroup = Field(default_factory=list) + """ + List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` that will be + executed after Actor. It constructs root + service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. + """ script: Union[Script, Dict] + """ + (required) A :py:class:`~.Script` instance (object or dict). + """ start_label: NodeLabel2Type + """ + (required) Actor start label. + """ fallback_label: Optional[NodeLabel2Type] = None + """ + Actor fallback label. + """ label_priority: float = 1.0 + """ + Default priority value for all actor :py:const:`labels ` + where there is no priority. Defaults to `1.0`. + """ condition_handler: Callable = Field(default=default_condition_handler) + """ + Handler that processes a call of actor condition functions. Defaults to `None`. + """ slots: GroupSlot = Field(default_factory=GroupSlot) + """Slots configuration.""" + # Docs could look like this for one-liners handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) + """ + This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~chatsky.script.Actor`. + + - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. + - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + """ messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) + """ + An `AbsMessagingInterface` instance for this pipeline. + """ context_storage: Union[DBContextStorage, Dict] = Field(default_factory=dict) + """ + A :py:class:`~.DBContextStorage` instance for this pipeline or + a dict to store dialog :py:class:`~.Context`. + """ before_handler: ComponentExtraHandler = Field(default_factory=list) + """ + List of `_ComponentExtraHandler` to add to the group. + """ after_handler: ComponentExtraHandler = Field(default_factory=list) + """ + List of `_ComponentExtraHandler` to add to the group. + """ timeout: Optional[float] = None + """ + Timeout to add to pipeline root service group. + """ optimization_warnings: bool = False + """ + Asynchronous pipeline optimization check request flag; + warnings will be sent to logs. Additionally, it has some calculated fields: + + - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, + - `actor` is a pipeline actor, found among services. + """ parallelize_processing: bool = False + """ + This flag determines whether or not the functions + defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections + of the script should be parallelized over respective groups. + """ _clean_turn_cache: Optional[bool] @computed_field @@ -128,7 +153,7 @@ def _services_pipeline(self) -> ServiceGroup: return services_pipeline @model_validator(mode="after") - def pipeline_init(self): + def __pipeline_init(self): finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) if self.optimization_warnings: diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 6d3d9936b..ae65c9e2f 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -38,16 +38,23 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T """ functions: List[ExtraHandlerFunction] = Field(default_factory=list) - """A list or instance of :py:data:`~.ExtraHandlerFunction`. - :type functions: :py:data:`~.ExtraHandlerFunction`""" + """ + A list or instance of :py:data:`~.ExtraHandlerFunction`. + """ stage: ClassVar[ExtraHandlerType] = ExtraHandlerType.UNDEFINED - """An :py:class:`~.ExtraHandlerType`, specifying whether this handler will - be executed before or after pipeline component.""" + """ + An :py:class:`~.ExtraHandlerType`, specifying whether this handler will + be executed before or after pipeline component. + """ timeout: Optional[float] = None - """(for asynchronous only!) Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted.""" + """ + (for asynchronous only!) Maximum component execution time (in seconds), + if it exceeds this time, it is interrupted. + """ requested_async_flag: Optional[bool] = None - """Requested asynchronous property.""" + """ + Requested asynchronous property. + """ @model_validator(mode="before") @classmethod diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 8b8e8f94b..42c58e19a 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -64,7 +64,7 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod - def components_constructor(cls, data: Any): + def __components_constructor(cls, data: Any): if isinstance(data, (list, PipelineComponent, Callable)): result = {"components": data} elif isinstance(data, dict): @@ -80,7 +80,7 @@ def components_constructor(cls, data: Any): return result @model_validator(mode="after") - def calculate_async_flag(self): + def __calculate_async_flag(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) return self diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index c847a7923..86c845e56 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -59,7 +59,7 @@ class Service(PipelineComponent): @model_validator(mode="before") @classmethod - def handler_constructor(cls, data: Any): + def __handler_constructor(cls, data: Any): if isinstance(data, Callable): return {"handler": data} elif not isinstance(data, dict): @@ -67,7 +67,7 @@ def handler_constructor(cls, data: Any): return data @model_validator(mode="after") - def tick_async_flag(self): + def __tick_async_flag(self): self.calculated_async_flag = True return self From 5cc6717179f9b8c0922fd4c1a37679832011696f Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 16:54:49 +0300 Subject: [PATCH 58/86] minor docs fix --- chatsky/pipeline/pipeline/actor.py | 15 +++++++-------- chatsky/pipeline/pipeline/component.py | 7 ++++--- chatsky/pipeline/pipeline/pipeline.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 6905b5ecf..acb8bfc5a 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -80,14 +80,13 @@ class Actor(PipelineComponent): fallback_label: Optional[NodeLabel2Type] = None """ The label of :py:class:`~chatsky.script.Script`. - Dialog comes into that label if all other transitions failed, - or there was an error while executing the scenario. - Defaults to `None`. + Dialog comes into that label if all other transitions failed, + or there was an error while executing the scenario. Defaults to `None`. """ label_priority: float = 1.0 """ Default priority value for all :py:const:`labels ` - where there is no priority. Defaults to `1.0`. + where there is no priority. Defaults to `1.0`. """ condition_handler: Callable = Field(default=default_condition_handler) """ @@ -96,7 +95,7 @@ class Actor(PipelineComponent): handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) """ This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. + the certain stages of work of :py:class:`~chatsky.script.Actor`. - key (:py:class:`~chatsky.script.ActorStage`) - Stage in which the handler is called. - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. @@ -105,12 +104,12 @@ class Actor(PipelineComponent): _clean_turn_cache: bool = True @model_validator(mode="after") - def __tick_async_flag(self): + def __tick_async_flag__(self): self.calculated_async_flag = False return self @model_validator(mode="after") - def __start_label_validator(self): + def __start_label_validator__(self): if not isinstance(self.script, Script): self.script = Script(script=self.script) self.start_label = normalize_label(self.start_label) @@ -119,7 +118,7 @@ def __start_label_validator(self): return self @model_validator(mode="after") - def __fallback_label_validator(self): + def __fallback_label_validator__(self): if self.fallback_label is None: self.fallback_label = self.start_label else: diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 21b42f23a..ebee91be4 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -53,7 +53,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo timeout: Optional[float] = None """ (for asynchronous only!) Maximum component execution time (in seconds), - if it exceeds this time, it is interrupted. + if it exceeds this time, it is interrupted. """ requested_async_flag: Optional[bool] = None """ @@ -63,8 +63,9 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo calculated_async_flag: bool = False """ Whether the component can be asynchronous or not - 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, - 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. + + 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, + 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. """ start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) """ diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 31e65544d..925585573 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -49,13 +49,13 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): pre_services: ServiceGroup = Field(default_factory=list) """ List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` - that will be executed before Actor. + that will be executed before Actor. """ post_services: ServiceGroup = Field(default_factory=list) """ List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` that will be - executed after Actor. It constructs root - service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. + executed after Actor. It constructs root + service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. """ script: Union[Script, Dict] """ @@ -72,7 +72,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): label_priority: float = 1.0 """ Default priority value for all actor :py:const:`labels ` - where there is no priority. Defaults to `1.0`. + where there is no priority. Defaults to `1.0`. """ condition_handler: Callable = Field(default=default_condition_handler) """ @@ -84,7 +84,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) """ This variable is responsible for the usage of external handlers on - the certain stages of work of :py:class:`~chatsky.script.Actor`. + the certain stages of work of :py:class:`~chatsky.script.Actor`. - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. @@ -96,7 +96,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): context_storage: Union[DBContextStorage, Dict] = Field(default_factory=dict) """ A :py:class:`~.DBContextStorage` instance for this pipeline or - a dict to store dialog :py:class:`~.Context`. + a dict to store dialog :py:class:`~.Context`. """ before_handler: ComponentExtraHandler = Field(default_factory=list) """ @@ -113,7 +113,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): optimization_warnings: bool = False """ Asynchronous pipeline optimization check request flag; - warnings will be sent to logs. Additionally, it has some calculated fields: + warnings will be sent to logs. Additionally, it has some calculated fields: - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, - `actor` is a pipeline actor, found among services. @@ -121,8 +121,8 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): parallelize_processing: bool = False """ This flag determines whether or not the functions - defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections - of the script should be parallelized over respective groups. + defined in the ``PRE_RESPONSE_PROCESSING`` and ``PRE_TRANSITIONS_PROCESSING`` sections + of the script should be parallelized over respective groups. """ _clean_turn_cache: Optional[bool] From 2b04dfa17956498a087fec5b2377e5376537fb7c Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 16:56:13 +0300 Subject: [PATCH 59/86] lint --- chatsky/pipeline/pipeline/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index ebee91be4..def00e436 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -63,7 +63,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo calculated_async_flag: bool = False """ Whether the component can be asynchronous or not - + 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. """ From c5ca8ba4305bdf1f7a5fa92ebe11045adc13fea3 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 17:42:11 +0300 Subject: [PATCH 60/86] trying to inherit PipelineComponent fields in Service, ServiceGroup --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 842829391..027804ae1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -153,6 +153,7 @@ "private-members": True, "member-order": "bysource", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", + "inherited-members": "PipelineComponent", } From a5d3b3371b8eb8c0404c44aaab91dad90e278cc8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 18:05:26 +0300 Subject: [PATCH 61/86] fixing new autodocs --- chatsky/pipeline/pipeline/actor.py | 5 +++-- chatsky/pipeline/pipeline/component.py | 7 ++++--- chatsky/pipeline/pipeline/pipeline.py | 14 +++++++++----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index acb8bfc5a..ce8d875d9 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -97,8 +97,9 @@ class Actor(PipelineComponent): This variable is responsible for the usage of external handlers on the certain stages of work of :py:class:`~chatsky.script.Actor`. - - key (:py:class:`~chatsky.script.ActorStage`) - Stage in which the handler is called. - - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. + - key (:py:class:`~chatsky.script.ActorStage`) - Stage in which the handler is called. + - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. + """ # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! _clean_turn_cache: bool = True diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index def00e436..c44dc20f7 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -62,20 +62,21 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo """ calculated_async_flag: bool = False """ - Whether the component can be asynchronous or not + Whether the component can be asynchronous or not. 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. + """ start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) """ StartConditionCheckerFunction that is invoked before each component execution; - component is executed only if it returns `True`. + component is executed only if it returns `True`. """ name: Optional[str] = None """ Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), - should not be blank or contain `.` symbol. + should not be blank or contain `.` symbol. """ path: Optional[str] = None """ diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 925585573..4b049c538 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -79,15 +79,18 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): Handler that processes a call of actor condition functions. Defaults to `None`. """ slots: GroupSlot = Field(default_factory=GroupSlot) - """Slots configuration.""" + """ + Slots configuration. + """ # Docs could look like this for one-liners handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) """ This variable is responsible for the usage of external handlers on the certain stages of work of :py:class:`~chatsky.script.Actor`. - - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. - - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + - key: :py:class:`~chatsky.script.ActorStage` - Stage in which the handler is called. + - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + """ messenger_interface: MessengerInterface = Field(default_factory=CLIMessengerInterface) """ @@ -115,8 +118,9 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): Asynchronous pipeline optimization check request flag; warnings will be sent to logs. Additionally, it has some calculated fields: - - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, - - `actor` is a pipeline actor, found among services. + - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, + - `actor` is a pipeline actor, found among services. + """ parallelize_processing: bool = False """ From 314b9fc5963d42086a20a8b2be40f26df7fc7dcc Mon Sep 17 00:00:00 2001 From: ZergLev Date: Fri, 9 Aug 2024 18:12:17 +0300 Subject: [PATCH 62/86] checking if conf.py is the reason for it not working --- docs/source/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 027804ae1..842829391 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -153,7 +153,6 @@ "private-members": True, "member-order": "bysource", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", - "inherited-members": "PipelineComponent", } From 3130a82bef623d54bfe8a360ea75198760460a83 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 14:07:40 +0300 Subject: [PATCH 63/86] updated the basic pipeline_from_dict tutorial for now --- .../3_pipeline_dict_with_services_basic.py | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py index db082a3f1..f339771a7 100644 --- a/tutorials/pipeline/3_pipeline_dict_with_services_basic.py +++ b/tutorials/pipeline/3_pipeline_dict_with_services_basic.py @@ -31,11 +31,30 @@ # %% [markdown] """ -When Pipeline is created using Pydantic's `model_validate` method, -pipeline should be defined as a dictionary. -It may contain `pre-services` and 'post-services' - `ServiceGroup` objects, -basically a list of `Service` objects or more `ServiceGroup` objects, -see tutorial 4. +When Pipeline is created using it's constructor method or +Pydantic's `model_validate` method, +`Pipeline` should be defined as a dictionary of a particular structure, +which must contain `script`, `start_label` and `fallback_label`, +see `Script` tutorials. + +Optional Pipeline parameters: +* `messenger_interface` - `MessengerInterface` instance, + is used to connect to channel and transfer IO to user. +* `context_storage` - Place to store dialog contexts + (dictionary or a `DBContextStorage` instance). +* `pre-services` - A `ServiceGroup` object, + basically a list of `Service` objects or more `ServiceGroup` objects, + see tutorial 4. +* `post-services` - A `ServiceGroup` object, + basically a list of `Service` objects or more `ServiceGroup` objects, + see tutorial 4. +* `before_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. + See tutorials 6 and 7. +* `after_handler` - a list of `ExtraHandlerFunction` objects or + a `ComponentExtraHandler` object. + See tutorials 6 and 7. +* `timeout` - Pipeline timeout, see tutorial 5. On pipeline execution services from `components` = 'pre-services' + actor + 'post-services' @@ -48,8 +67,8 @@ for most cases `run` method should be used. It starts pipeline asynchronously and connects to provided messenger interface. -Here, the pipeline contains 4 services, -defined in 4 different ways with different signatures. +Here, the pipeline contains 3 services, +defined in 3 different ways with different signatures. """ @@ -77,16 +96,20 @@ def postprocess(_): "start_label": ("greeting_flow", "start_node"), "fallback_label": ("greeting_flow", "fallback_node"), "pre_services": [ - {"handler": prepreprocess}, + { + "handler": prepreprocess, + "name": "prepreprocessor", + }, preprocess, ], - "post_services": Service(handler=postprocess), + "post_services": Service(handler=postprocess, name="postprocessor"), } # %% -pipeline = Pipeline.model_validate(pipeline_dict) +pipeline = Pipeline(**pipeline_dict) # or -# pipeline = Pipeline(**pipeline_dict) +# pipeline = Pipeline.model_validate(pipeline_dict) + if __name__ == "__main__": check_happy_path(pipeline, HAPPY_PATH) From e7f1506122996d67a8089d7255f8f476ae5fe16e Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 14:57:28 +0300 Subject: [PATCH 64/86] trying to show inherited members in the docs except Pydantic.BaseModel and abc.ABC --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 842829391..f71be1fb4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -152,6 +152,7 @@ "undoc-members": False, "private-members": True, "member-order": "bysource", + "inherited-members": "Pydantic.BaseModel, abc.ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From 4a019a78d95302aecb1e2e44b362de2bfa39e551 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 15:17:25 +0300 Subject: [PATCH 65/86] fixing docs --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index f71be1fb4..eba6f4775 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -152,7 +152,7 @@ "undoc-members": False, "private-members": True, "member-order": "bysource", - "inherited-members": "Pydantic.BaseModel, abc.ABC", + "inherited-members": "BaseModel, ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From 75cb0b871d5503de25f69c765665a164057918f8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 15:37:52 +0300 Subject: [PATCH 66/86] trying with undoc-members == True --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index eba6f4775..77cb630dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -149,7 +149,7 @@ autodoc_default_options = { "members": True, - "undoc-members": False, + "undoc-members": True, "private-members": True, "member-order": "bysource", "inherited-members": "BaseModel, ABC", From 3922a030ea1f1022f81bf8bcce3e93550066eee5 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 15:49:50 +0300 Subject: [PATCH 67/86] reverting redundant change --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 77cb630dd..eba6f4775 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -149,7 +149,7 @@ autodoc_default_options = { "members": True, - "undoc-members": True, + "undoc-members": False, "private-members": True, "member-order": "bysource", "inherited-members": "BaseModel, ABC", From 2aacdbba51d84f8719a352891960953fb688aae9 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 15:55:23 +0300 Subject: [PATCH 68/86] trying out member-order == groupwise --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index eba6f4775..cc9fa4c57 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -151,7 +151,7 @@ "members": True, "undoc-members": False, "private-members": True, - "member-order": "bysource", + "member-order": "groupwise", "inherited-members": "BaseModel, ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From a4b55b2e0679f15274f9bebdf507e882e7a2a8f9 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 16:16:23 +0300 Subject: [PATCH 69/86] reverted change for now, didn't work as needed --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index cc9fa4c57..eba6f4775 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -151,7 +151,7 @@ "members": True, "undoc-members": False, "private-members": True, - "member-order": "groupwise", + "member-order": "bysource", "inherited-members": "BaseModel, ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From 5cf3dbcbc59d119aa5df67c40f59b79491283a8f Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 16:28:31 +0300 Subject: [PATCH 70/86] testing a possible docs format --- chatsky/pipeline/service/service.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 86c845e56..ebb26b9c8 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -14,7 +14,7 @@ import logging import inspect from typing import TYPE_CHECKING, Any, Optional, Callable -from pydantic import model_validator +from pydantic import model_validator, Field from chatsky.script import Context @@ -40,22 +40,20 @@ class Service(PipelineComponent): Service can be included into pipeline as object or a dictionary. Service group can be synchronous or asynchronous. Service can be asynchronous only if its handler is a coroutine. - - :param handler: A service function or an actor. - :type handler: :py:data:`~.ServiceFunction` - :param before_handler: List of `_ComponentExtraHandler` to add to the group. - :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param after_handler: List of `_ComponentExtraHandler` to add to the group. - :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param timeout: Timeout to add to the group. - :param requested_async_flag: Requested asynchronous property. - :param start_condition: StartConditionCheckerFunction that is invoked before each service execution; - service is executed only if it returns `True`. - :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`] - :param name: Requested service name. """ handler: ServiceFunction + """ + A service function. + """ + before_handler: BeforeHandler = Field(default_factory=BeforeHandler) + after_handler: AfterHandler = Field(default_factory=AfterHandler) + timeout: Optional[float] = None + requested_async_flag: Optional[bool] = None + calculated_async_flag: bool = False + start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) + name: Optional[str] = None + path: Optional[str] = None @model_validator(mode="before") @classmethod From 2ff67528a73c2534908f8b237f01786c15864228 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 16:43:59 +0300 Subject: [PATCH 71/86] trying to inherit only fields --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index eba6f4775..7fd814640 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -152,7 +152,7 @@ "undoc-members": False, "private-members": True, "member-order": "bysource", - "inherited-members": "BaseModel, ABC", + # "inherited-members": "BaseModel, ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From f026b9e019fd01c2225603c7ebdbf86a41eeb2de Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 17:07:55 +0300 Subject: [PATCH 72/86] documentation format fixed, but review required --- chatsky/pipeline/service/group.py | 28 +++++++++++++++------------- chatsky/pipeline/service/service.py | 2 +- docs/source/conf.py | 1 - 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 42c58e19a..d8c544f64 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -12,7 +12,9 @@ import asyncio import logging from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional, Callable -from pydantic import model_validator + +from chatsky.pipeline import BeforeHandler, AfterHandler, always_start_condition +from pydantic import model_validator, Field from chatsky.script import Context from ..pipeline.actor import Actor @@ -23,6 +25,7 @@ GlobalExtraHandlerType, ExtraHandlerConditionFunction, ExtraHandlerFunction, + StartConditionCheckerFunction, ) from .service import Service @@ -40,18 +43,6 @@ class ServiceGroup(PipelineComponent): Components in synchronous groups are executed consequently (no matter is they are synchronous or asynchronous). Components in asynchronous groups are executed simultaneously. Group can be asynchronous only if all components in it are asynchronous. - - :param components: A `ServiceGroup` object, that will be added to the group. - :type components: :py:data:`~.ServiceGroup` - :param before_handler: List of `_ComponentExtraHandler` to add to the group. - :type before_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param after_handler: List of `_ComponentExtraHandler` to add to the group. - :type after_handler: Optional[:py:data:`~._ComponentExtraHandler`] - :param timeout: Timeout to add to the group. - :param requested_async_flag: Requested asynchronous property. - :param start_condition: :py:data:`~.StartConditionCheckerFunction` that is invoked before each group execution; - group is executed only if it returns `True`. - :param name: Requested group name. """ components: List[ @@ -61,6 +52,17 @@ class ServiceGroup(PipelineComponent): ServiceGroup, ] ] + """ + A `ServiceGroup` object, that will be added to the group. + """ + # Inherited fields repeated. Don't delete these, they're needed for documentation! + before_handler: BeforeHandler = Field(default_factory=BeforeHandler) + after_handler: AfterHandler = Field(default_factory=AfterHandler) + timeout: Optional[float] = None + requested_async_flag: Optional[bool] = None + start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) + name: Optional[str] = None + path: Optional[str] = None @model_validator(mode="before") @classmethod diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index ebb26b9c8..c2c7ffb02 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -46,11 +46,11 @@ class Service(PipelineComponent): """ A service function. """ + # Inherited fields repeated. Don't delete these, they're needed for documentation! before_handler: BeforeHandler = Field(default_factory=BeforeHandler) after_handler: AfterHandler = Field(default_factory=AfterHandler) timeout: Optional[float] = None requested_async_flag: Optional[bool] = None - calculated_async_flag: bool = False start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) name: Optional[str] = None path: Optional[str] = None diff --git a/docs/source/conf.py b/docs/source/conf.py index 7fd814640..842829391 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -152,7 +152,6 @@ "undoc-members": False, "private-members": True, "member-order": "bysource", - # "inherited-members": "BaseModel, ABC", "exclude-members": "_abc_impl, model_fields, model_computed_fields, model_config", } From 9278dca97ef341810e47d81ff121e4008a5b05f1 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Mon, 12 Aug 2024 17:35:51 +0300 Subject: [PATCH 73/86] minor fix --- chatsky/__rebuild_pydantic_models__.py | 2 -- chatsky/pipeline/__init__.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chatsky/__rebuild_pydantic_models__.py b/chatsky/__rebuild_pydantic_models__.py index 1db55a27a..f648d6449 100644 --- a/chatsky/__rebuild_pydantic_models__.py +++ b/chatsky/__rebuild_pydantic_models__.py @@ -1,11 +1,9 @@ # flake8: noqa: F401 from chatsky.pipeline import Pipeline -from chatsky.pipeline.pipeline.component import PipelineComponent from chatsky.pipeline.types import ExtraHandlerRuntimeInfo, StartConditionCheckerFunction from chatsky.script import Context, Script -PipelineComponent.model_rebuild() Pipeline.model_rebuild() Script.model_rebuild() Context.model_rebuild() diff --git a/chatsky/pipeline/__init__.py b/chatsky/pipeline/__init__.py index 680e2a668..c6152f31c 100644 --- a/chatsky/pipeline/__init__.py +++ b/chatsky/pipeline/__init__.py @@ -22,9 +22,9 @@ ServiceFunction, ) -from .pipeline.pipeline import Pipeline -from .pipeline.actor import Actor - from .service.extra import BeforeHandler, AfterHandler, ComponentExtraHandler -from .service.group import ServiceGroup from .service.service import Service, to_service +from .service.group import ServiceGroup + +from .pipeline.actor import Actor +from .pipeline.pipeline import Pipeline From 63a829d9602f74a26db1ce1f81a90288af981f12 Mon Sep 17 00:00:00 2001 From: ZergLev <64711614+ZergLev@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:30:56 +0300 Subject: [PATCH 74/86] Update chatsky/pipeline/pipeline/component.py Co-authored-by: Roman Zlobin --- chatsky/pipeline/pipeline/component.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index c44dc20f7..3bc471b24 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -171,8 +171,9 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[Comp @property def computed_name(self) -> str: """ - Every derivative of `PipelineComponent` must define this property. - :return: `str`. + Default name that is used if `self.name` is not defined. + In case two components in a `ServiceGroup` have the same `computed_name` + an incrementing number is appended to the name. """ return "noname_service" # Or could do the following: From 33b8de6edb26c54893088a700a3d53fefdea2a8f Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 14 Aug 2024 14:59:57 +0300 Subject: [PATCH 75/86] doing review changes, type validation bug accidentally created --- chatsky/pipeline/pipeline/actor.py | 16 +++++++++------ chatsky/pipeline/pipeline/component.py | 27 +++++++++++--------------- chatsky/pipeline/pipeline/pipeline.py | 15 +++++++------- chatsky/pipeline/pipeline/utils.py | 7 ++----- chatsky/pipeline/service/extra.py | 21 ++++++++------------ chatsky/pipeline/service/group.py | 20 +++++++------------ chatsky/pipeline/service/service.py | 5 +++-- tests/pipeline/test_validation.py | 7 +++++++ 8 files changed, 55 insertions(+), 63 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index ce8d875d9..330a99730 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -111,6 +111,11 @@ def __tick_async_flag__(self): @model_validator(mode="after") def __start_label_validator__(self): + """ + Validates :py:data:`~.Actor.start_label`. In case requested + `start_label` doesn't exist in the given :py:class:`~.Script`, + raises ValueError. + """ if not isinstance(self.script, Script): self.script = Script(script=self.script) self.start_label = normalize_label(self.start_label) @@ -120,6 +125,11 @@ def __start_label_validator__(self): @model_validator(mode="after") def __fallback_label_validator__(self): + """ + Validates :py:data:`~.Actor.fallback_label`. In case requested + `fallback_label` doesn't exist in the given :py:class:`~.Script`, + raises ValueError. + """ if self.fallback_label is None: self.fallback_label = self.start_label else: @@ -133,12 +143,6 @@ def computed_name(self) -> str: return "actor" async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: - """ - Method for running an `Actor`. - - :param pipeline: Current pipeline. - :param ctx: Current dialog context. - """ await self._run_handlers(ctx, pipeline, ActorStage.CONTEXT_INIT) # get previous node diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index c44dc20f7..4cfed9b49 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -70,7 +70,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo """ start_condition: StartConditionCheckerFunction = Field(default=always_start_condition) """ - StartConditionCheckerFunction that is invoked before each component execution; + :py:class:`~.pipeline.types.StartConditionCheckerFunction` that is invoked before each component execution; component is executed only if it returns `True`. """ name: Optional[str] = None @@ -85,6 +85,11 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo @model_validator(mode="after") def __pipeline_component_validator(self): + """ + Validates this component. Raises `ValueError` if component's + name is blank or if it contains dots. In case component can't be async + but was requested to be, raises an `Exception`. + """ if self.name is not None: if self.name == "": raise ValueError("Name cannot be blank.") @@ -152,27 +157,17 @@ async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipelin @abc.abstractmethod async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[ComponentExecutionState]: """ - Method for running this component. It can be an Actor, Service or ServiceGroup. - It has to be defined in the child classes, - which is done in each of the default PipelineComponents. - Service 'handler' has three possible signatures. These possible signatures are: - - - (ctx: Context) - accepts current dialog context only. - - (ctx: Context, pipeline: Pipeline) - accepts context and current pipeline. - - | (ctx: Context, pipeline: Pipeline, info: ServiceRuntimeInfo) - accepts context, - pipeline and service runtime info dictionary. - - :param ctx: Current dialog context. - :param pipeline: The current pipeline. - :return: `None` + Run this component. + + :param ctx: Current dialog :py:class:`~.Context`. + :param pipeline: This :py:class:`~.Pipeline`. """ raise NotImplementedError @property def computed_name(self) -> str: """ - Every derivative of `PipelineComponent` must define this property. - :return: `str`. + Every derivative of :py:class:`~.PipelineComponent` must define this property. """ return "noname_service" # Or could do the following: diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index 4b049c538..b584ed6bd 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -54,7 +54,7 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): post_services: ServiceGroup = Field(default_factory=list) """ List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` that will be - executed after Actor. It constructs root + executed after :py:class:`~.Actor`. It constructs root service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. """ script: Union[Script, Dict] @@ -63,15 +63,15 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ start_label: NodeLabel2Type """ - (required) Actor start label. + (required) :py:class:`~.Actor` start label. """ fallback_label: Optional[NodeLabel2Type] = None """ - Actor fallback label. + :py:class:`~.Actor` fallback label. """ label_priority: float = 1.0 """ - Default priority value for all actor :py:const:`labels ` + Default priority value for all actor :py:const:`labels ` where there is no priority. Defaults to `1.0`. """ condition_handler: Callable = Field(default=default_condition_handler) @@ -82,7 +82,6 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ Slots configuration. """ - # Docs could look like this for one-liners handlers: Dict[ActorStage, List[Callable]] = Field(default_factory=dict) """ This variable is responsible for the usage of external handlers on @@ -103,11 +102,11 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): """ before_handler: ComponentExtraHandler = Field(default_factory=list) """ - List of `_ComponentExtraHandler` to add to the group. + List of :py:class:`~._ComponentExtraHandler` to add to the group. """ after_handler: ComponentExtraHandler = Field(default_factory=list) """ - List of `_ComponentExtraHandler` to add to the group. + List of :py:class:`~._ComponentExtraHandler` to add to the group. """ timeout: Optional[float] = None """ @@ -157,7 +156,7 @@ def _services_pipeline(self) -> ServiceGroup: return services_pipeline @model_validator(mode="after") - def __pipeline_init(self): + def __pipeline_init__(self): finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) if self.optimization_warnings: diff --git a/chatsky/pipeline/pipeline/utils.py b/chatsky/pipeline/pipeline/utils.py index 2761fccbb..931bade56 100644 --- a/chatsky/pipeline/pipeline/utils.py +++ b/chatsky/pipeline/pipeline/utils.py @@ -21,7 +21,7 @@ def rename_component_incrementing(component: PipelineComponent, collisions: List - If component is an `Actor`, it is named `actor`. - If component is a `Service` and the service's handler is `Callable`, it is named after this `callable`. - If it's a service group, it is named `service_group`. - - Otherwise, it is names `noname_service`. + - Otherwise, it is named `noname_service`. - | After that, `_[NUMBER]` is added to the resulting name, where `_[NUMBER]` is number of components with the same name in current service group. @@ -29,10 +29,7 @@ def rename_component_incrementing(component: PipelineComponent, collisions: List :param collisions: Components in the same service group as component. :return: Generated name """ - if isinstance(component, PipelineComponent): - base_name = component.computed_name - else: - base_name = "noname_service" + base_name = component.computed_name name_index = 0 while f"{base_name}_{name_index}" in [component.name for component in collisions]: name_index += 1 diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index ae65c9e2f..a951b67a5 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -59,19 +59,14 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @model_validator(mode="before") @classmethod def functions_constructor(cls, data: Any): - if isinstance(data, (list, Callable)): - result = {"functions": data} - elif isinstance(data, dict): - result = data.copy() - else: - raise ValueError( - "Extra Handler can only be initialized from a Dict," - " a Callable or a list of Callables. Wrong inputs received." - ) - - if ("functions" in result) and (not isinstance(result["functions"], list)): - result["functions"] = [result["functions"]] - return result + """ + Adds support for initializing from a `Callable` or List[`Callable`]. + """ + if isinstance(data, list): + return {"functions": data} + if callable(data): + return {"functions": [data]} + return data @computed_field(repr=False) def calculated_async_flag(self) -> bool: diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index d8c544f64..cf3492a8f 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -67,19 +67,13 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod def __components_constructor(cls, data: Any): - if isinstance(data, (list, PipelineComponent, Callable)): - result = {"components": data} - elif isinstance(data, dict): - result = data.copy() - else: - raise ValueError( - "Service Group can only be initialized from a Dict," - " a PipelineComponent or a list of PipelineComponents. Wrong inputs received." - ) - - if ("components" in result) and (not isinstance(result["components"], list)): - result["components"] = [result["components"]] - return result + """Adds support for initializing from a `Callable`, `List` + and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`)""" + if isinstance(data, list): + return {"components": data} + if callable(data) or isinstance(data, PipelineComponent): + return {"components": [data]} + return data @model_validator(mode="after") def __calculate_async_flag(self): diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index c2c7ffb02..6a0638cf7 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -58,10 +58,11 @@ class Service(PipelineComponent): @model_validator(mode="before") @classmethod def __handler_constructor(cls, data: Any): + """ + Adds support for initializing from a `Callable`. + """ if isinstance(data, Callable): return {"handler": data} - elif not isinstance(data, dict): - raise ValueError("A Service can only be initialized from a Dict or a Callable." " Wrong inputs received.") return data @model_validator(mode="after") diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index e32d2146f..4b4b1de8a 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -145,6 +145,13 @@ def test_correct_inputs(self): Pipeline(**TOY_SCRIPT_KWARGS) Pipeline.model_validate(TOY_SCRIPT_KWARGS) + # Testing if actor is an unchangeable constant throughout the program + def test_cached_property(self): + pipeline = Pipeline(**TOY_SCRIPT_KWARGS) + old_actor_id = id(pipeline.actor) + pipeline.start_label = ("greeting_flow", "fallback_node") + assert old_actor_id == id(pipeline.actor) + def test_pre_services(self): with pytest.raises(ValidationError): # 'pre_services' must be a ServiceGroup From 0bab5bad0e32d64835f1d82e53a0a4c945651f86 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 14 Aug 2024 15:10:43 +0300 Subject: [PATCH 76/86] type validation bug fixed --- chatsky/pipeline/service/extra.py | 14 ++++++++++---- chatsky/pipeline/service/group.py | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index a951b67a5..c8aeb9540 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -63,10 +63,16 @@ def functions_constructor(cls, data: Any): Adds support for initializing from a `Callable` or List[`Callable`]. """ if isinstance(data, list): - return {"functions": data} - if callable(data): - return {"functions": [data]} - return data + result = {"functions": data} + elif callable(data): + result = {"functions": [data]} + else: + result = data + + if isinstance(result, dict): + if ("functions" in result) and (not isinstance(result["functions"], list)): + result["functions"] = [result["functions"]] + return result @computed_field(repr=False) def calculated_async_flag(self) -> bool: diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index cf3492a8f..59c6cff6b 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -70,10 +70,16 @@ def __components_constructor(cls, data: Any): """Adds support for initializing from a `Callable`, `List` and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`)""" if isinstance(data, list): - return {"components": data} - if callable(data) or isinstance(data, PipelineComponent): - return {"components": [data]} - return data + result = {"components": data} + elif callable(data) or isinstance(data, PipelineComponent): + result = {"components": [data]} + else: + result = data + + if isinstance(result, dict): + if ("components" in result) and (not isinstance(result["components"], list)): + result["components"] = [result["components"]] + return result @model_validator(mode="after") def __calculate_async_flag(self): From e6eae9646261e2ee16ab822ecc924a45104fc2bb Mon Sep 17 00:00:00 2001 From: ZergLev Date: Wed, 14 Aug 2024 15:12:37 +0300 Subject: [PATCH 77/86] lint --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index c8aeb9540..9d9e7ed35 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -10,7 +10,7 @@ import asyncio import logging import inspect -from typing import Optional, List, TYPE_CHECKING, Any, ClassVar, Callable +from typing import Optional, List, TYPE_CHECKING, Any, ClassVar from pydantic import BaseModel, computed_field, model_validator, Field from chatsky.script import Context diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 59c6cff6b..1e39fdf47 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio import logging -from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional, Callable +from typing import List, Union, Awaitable, TYPE_CHECKING, Any, Optional from chatsky.pipeline import BeforeHandler, AfterHandler, always_start_condition from pydantic import model_validator, Field From 0f6839def870affb980da658f5a21b991152e6e8 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 15 Aug 2024 12:14:37 +0300 Subject: [PATCH 78/86] minor changes + added missing links --- chatsky/pipeline/pipeline/actor.py | 2 +- chatsky/pipeline/pipeline/component.py | 11 ++++------- chatsky/pipeline/pipeline/pipeline.py | 4 ++-- chatsky/pipeline/service/extra.py | 1 + chatsky/pipeline/service/group.py | 11 +++++++---- chatsky/pipeline/service/service.py | 11 ++++------- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 330a99730..3d82cfe52 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -98,7 +98,7 @@ class Actor(PipelineComponent): the certain stages of work of :py:class:`~chatsky.script.Actor`. - key (:py:class:`~chatsky.script.ActorStage`) - Stage in which the handler is called. - - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. + - value (`List[Callable]`) - The list of called handlers for each stage. Defaults to an empty `dict`. """ # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index c7bcb6a34..f8efe55ba 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -58,7 +58,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo requested_async_flag: Optional[bool] = None """ Requested asynchronous property; if not defined, - `calculated_async_flag` is used instead. + :py:attr:`~.PipelineComponent.calculated_async_flag` is used instead. """ calculated_async_flag: bool = False """ @@ -167,14 +167,11 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[Comp @property def computed_name(self) -> str: """ - Default name that is used if `self.name` is not defined. - In case two components in a `ServiceGroup` have the same `computed_name` - an incrementing number is appended to the name. + Default name that is used if :py:attr:`~.PipelineComponent.name` is not defined. + In case two components in a :py:class:`~.ServiceGroup` have the same + :py:attr:`~.PipelineComponent.computed_name` an incrementing number is appended to the name. """ return "noname_service" - # Or could do the following: - # raise NotImplementedError - # But this default value makes sense and replicates previous logic. async def _run(self, ctx: Context, pipeline: Pipeline) -> None: """ diff --git a/chatsky/pipeline/pipeline/pipeline.py b/chatsky/pipeline/pipeline/pipeline.py index b584ed6bd..036e0e286 100644 --- a/chatsky/pipeline/pipeline/pipeline.py +++ b/chatsky/pipeline/pipeline/pipeline.py @@ -48,12 +48,12 @@ class Pipeline(BaseModel, extra="forbid", arbitrary_types_allowed=True): pre_services: ServiceGroup = Field(default_factory=list) """ - List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` + List of :py:class:`~.Service` or :py:class:`~.ServiceGroup` that will be executed before Actor. """ post_services: ServiceGroup = Field(default_factory=list) """ - List of :py:data:`~.Service` or :py:data:`~.ServiceGroup` that will be + List of :py:class:`~.Service` or :py:class:`~.ServiceGroup` that will be executed after :py:class:`~.Actor`. It constructs root service group by merging `pre_services` + actor + `post_services`. It will always be named pipeline. """ diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 9d9e7ed35..c1ea09f98 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -61,6 +61,7 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T def functions_constructor(cls, data: Any): """ Adds support for initializing from a `Callable` or List[`Callable`]. + Casts `functions` to `list` if it's not already. """ if isinstance(data, list): result = {"functions": data} diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 1e39fdf47..5cc6bf96f 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -53,7 +53,7 @@ class ServiceGroup(PipelineComponent): ] ] """ - A `ServiceGroup` object, that will be added to the group. + A :py:class:`~.ServiceGroup` object, that will be added to the group. """ # Inherited fields repeated. Don't delete these, they're needed for documentation! before_handler: BeforeHandler = Field(default_factory=BeforeHandler) @@ -67,8 +67,11 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod def __components_constructor(cls, data: Any): - """Adds support for initializing from a `Callable`, `List` - and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`)""" + """ + Adds support for initializing from a `Callable`, `List` + and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`) + Casts `components` to `list` if it's not already. + """ if isinstance(data, list): result = {"components": data} elif callable(data) or isinstance(data, PipelineComponent): @@ -90,7 +93,7 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> Optional[Comp """ Method for running this service group. Catches runtime exceptions and logs them. It doesn't include extra handlers execution, start condition checking or error handling - pure execution only. - Executes components inside the group based on its `asynchronous` property. + Executes components inside the group based on its :py:attr:`~.PipelineComponent.asynchronous` property. Collects information about their execution state - group is finished successfully only if all components in it finished successfully. diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 6a0638cf7..5bdb7086a 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -44,7 +44,7 @@ class Service(PipelineComponent): handler: ServiceFunction """ - A service function. + A :py:data:`~.ServiceFunction`. """ # Inherited fields repeated. Don't delete these, they're needed for documentation! before_handler: BeforeHandler = Field(default_factory=BeforeHandler) @@ -96,13 +96,10 @@ async def run_component(self, ctx: Context, pipeline: Pipeline) -> None: @property def computed_name(self) -> str: - if callable(self.handler): - if inspect.isfunction(self.handler): - return self.handler.__name__ - else: - return self.handler.__class__.__name__ + if inspect.isfunction(self.handler): + return self.handler.__name__ else: - return "noname_service" + return self.handler.__class__.__name__ @property def info_dict(self) -> dict: From a04756fb177d7a827eef7fb8dfe1b5a290ea658f Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 15 Aug 2024 13:09:15 +0300 Subject: [PATCH 79/86] increased test coverage for PipelineComponent --- tests/pipeline/test_validation.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index 4b4b1de8a..e3c36371e 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -1,3 +1,6 @@ +from typing import Callable + +from chatsky.pipeline.pipeline.component import PipelineComponent from pydantic import ValidationError import pytest @@ -156,3 +159,26 @@ def test_pre_services(self): with pytest.raises(ValidationError): # 'pre_services' must be a ServiceGroup Pipeline(**TOY_SCRIPT_KWARGS, pre_services=123) + + +class CustomPipelineComponent(PipelineComponent): + start_condition: Callable = lambda: True + + def run_component(self, ctx: Context, pipeline: Pipeline): + pass + + +class TestPipelineComponentValidation: + CustomPipelineComponent.model_rebuild() + + def test_wrong_names(self): + func = UserFunctionSamples.correct_service_function_1 + with pytest.raises(ValidationError): + Service(handler=func, name="bad.name") + with pytest.raises(ValidationError): + Service(handler=func, name="") + + # Maybe this test should be in a different file, though. + def test_name_not_defined(self): + comp = CustomPipelineComponent() + assert comp.computed_name == "noname_service" From 64c8761732d26d04e58b21bef906a81635033e33 Mon Sep 17 00:00:00 2001 From: ZergLev Date: Thu, 15 Aug 2024 13:23:55 +0300 Subject: [PATCH 80/86] added #pragma: no cover to remove redundant lines from coverage --- chatsky/pipeline/conditions.py | 2 +- chatsky/pipeline/pipeline/actor.py | 2 +- chatsky/pipeline/pipeline/component.py | 2 +- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 2 +- chatsky/pipeline/types.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chatsky/pipeline/conditions.py b/chatsky/pipeline/conditions.py index 01a5acb45..38793aa9c 100644 --- a/chatsky/pipeline/conditions.py +++ b/chatsky/pipeline/conditions.py @@ -17,7 +17,7 @@ StartConditionCheckerAggregationFunction, ) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 3d82cfe52..c4436bc5b 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index f8efe55ba..bee16ab45 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index c1ea09f98..67bd298e7 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 5cc6bf96f..9d84a4db9 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 5bdb7086a..a95d4622f 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/types.py b/chatsky/pipeline/types.py index 56e955855..c275d242c 100644 --- a/chatsky/pipeline/types.py +++ b/chatsky/pipeline/types.py @@ -13,7 +13,7 @@ from pydantic import BaseModel -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from chatsky.pipeline.pipeline.pipeline import Pipeline from chatsky.script import Context, Message From 9d322a75bc38df2297adcd192ee91b8f3f7ef14c Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 01:03:30 +0300 Subject: [PATCH 81/86] Revert "added #pragma: no cover to remove redundant lines from coverage" This reverts commit 64c8761732d26d04e58b21bef906a81635033e33. --- chatsky/pipeline/conditions.py | 2 +- chatsky/pipeline/pipeline/actor.py | 2 +- chatsky/pipeline/pipeline/component.py | 2 +- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 2 +- chatsky/pipeline/types.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chatsky/pipeline/conditions.py b/chatsky/pipeline/conditions.py index 38793aa9c..01a5acb45 100644 --- a/chatsky/pipeline/conditions.py +++ b/chatsky/pipeline/conditions.py @@ -17,7 +17,7 @@ StartConditionCheckerAggregationFunction, ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index c4436bc5b..3d82cfe52 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index bee16ab45..f8efe55ba 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 67bd298e7..c1ea09f98 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 9d84a4db9..5cc6bf96f 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index a95d4622f..5bdb7086a 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline diff --git a/chatsky/pipeline/types.py b/chatsky/pipeline/types.py index c275d242c..56e955855 100644 --- a/chatsky/pipeline/types.py +++ b/chatsky/pipeline/types.py @@ -13,7 +13,7 @@ from pydantic import BaseModel -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from chatsky.pipeline.pipeline.pipeline import Pipeline from chatsky.script import Context, Message From c3f3d95bd975b375af1255006731bc1d134640ae Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 02:42:25 +0300 Subject: [PATCH 82/86] small changes to validator tests Why call `model_rebuild`? --- tests/pipeline/test_validation.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/pipeline/test_validation.py b/tests/pipeline/test_validation.py index e3c36371e..c245daed6 100644 --- a/tests/pipeline/test_validation.py +++ b/tests/pipeline/test_validation.py @@ -152,7 +152,7 @@ def test_correct_inputs(self): def test_cached_property(self): pipeline = Pipeline(**TOY_SCRIPT_KWARGS) old_actor_id = id(pipeline.actor) - pipeline.start_label = ("greeting_flow", "fallback_node") + pipeline.fallback_label = ("greeting_flow", "other_node") assert old_actor_id == id(pipeline.actor) def test_pre_services(self): @@ -169,8 +169,6 @@ def run_component(self, ctx: Context, pipeline: Pipeline): class TestPipelineComponentValidation: - CustomPipelineComponent.model_rebuild() - def test_wrong_names(self): func = UserFunctionSamples.correct_service_function_1 with pytest.raises(ValidationError): @@ -178,7 +176,7 @@ def test_wrong_names(self): with pytest.raises(ValidationError): Service(handler=func, name="") - # Maybe this test should be in a different file, though. + # todo: move this to component tests def test_name_not_defined(self): comp = CustomPipelineComponent() assert comp.computed_name == "noname_service" From b27961e0e47c8365ac73f8ebe6a66f64f8609de9 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 02:47:15 +0300 Subject: [PATCH 83/86] doc style: use `:raises:` directive and rephrase docstrings as commands https://peps.python.org/pep-0257/#one-line-docstrings --- chatsky/pipeline/pipeline/actor.py | 11 +++++------ chatsky/pipeline/pipeline/component.py | 7 ++++--- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/chatsky/pipeline/pipeline/actor.py b/chatsky/pipeline/pipeline/actor.py index 3d82cfe52..20800d6b2 100644 --- a/chatsky/pipeline/pipeline/actor.py +++ b/chatsky/pipeline/pipeline/actor.py @@ -112,9 +112,9 @@ def __tick_async_flag__(self): @model_validator(mode="after") def __start_label_validator__(self): """ - Validates :py:data:`~.Actor.start_label`. In case requested - `start_label` doesn't exist in the given :py:class:`~.Script`, - raises ValueError. + Validate :py:data:`~.Actor.start_label`. + + :raises ValueError: If `start_label` doesn't exist in the given :py:class:`~.Script`. """ if not isinstance(self.script, Script): self.script = Script(script=self.script) @@ -126,9 +126,8 @@ def __start_label_validator__(self): @model_validator(mode="after") def __fallback_label_validator__(self): """ - Validates :py:data:`~.Actor.fallback_label`. In case requested - `fallback_label` doesn't exist in the given :py:class:`~.Script`, - raises ValueError. + Validate :py:data:`~.Actor.fallback_label`. + :raises ValueError: If `fallback_label` doesn't exist in the given :py:class:`~.Script`. """ if self.fallback_label is None: self.fallback_label = self.start_label diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index f8efe55ba..330b56284 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -86,9 +86,10 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo @model_validator(mode="after") def __pipeline_component_validator(self): """ - Validates this component. Raises `ValueError` if component's - name is blank or if it contains dots. In case component can't be async - but was requested to be, raises an `Exception`. + Validate this component. + + :raises ValueError: If component's name is blank or if it contains dots. + :raises Exception: In case component can't be async, but was requested to be. """ if self.name is not None: if self.name == "": diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index c1ea09f98..1b90e5611 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -60,7 +60,7 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @classmethod def functions_constructor(cls, data: Any): """ - Adds support for initializing from a `Callable` or List[`Callable`]. + Add support for initializing from a `Callable` or List[`Callable`]. Casts `functions` to `list` if it's not already. """ if isinstance(data, list): diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 5cc6bf96f..9464e793d 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -68,7 +68,7 @@ class ServiceGroup(PipelineComponent): @classmethod def __components_constructor(cls, data: Any): """ - Adds support for initializing from a `Callable`, `List` + Add support for initializing from a `Callable`, `List` and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`) Casts `components` to `list` if it's not already. """ diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 5bdb7086a..975a3c3a0 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -59,7 +59,7 @@ class Service(PipelineComponent): @classmethod def __handler_constructor(cls, data: Any): """ - Adds support for initializing from a `Callable`. + Add support for initializing from a `Callable`. """ if isinstance(data, Callable): return {"handler": data} From de9eb83e9278e9631e25090cbbb902ac40ba6c91 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 02:48:15 +0300 Subject: [PATCH 84/86] hide some validators --- chatsky/pipeline/pipeline/component.py | 2 +- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 330b56284..4af186b05 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -84,7 +84,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo """ @model_validator(mode="after") - def __pipeline_component_validator(self): + def __pipeline_component_validator__(self): """ Validate this component. diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 9464e793d..2e5375a0d 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -85,7 +85,7 @@ def __components_constructor(cls, data: Any): return result @model_validator(mode="after") - def __calculate_async_flag(self): + def __calculate_async_flag__(self): self.calculated_async_flag = all([service.asynchronous for service in self.components]) return self diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 975a3c3a0..3c661af55 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -66,7 +66,7 @@ def __handler_constructor(cls, data: Any): return data @model_validator(mode="after") - def __tick_async_flag(self): + def __tick_async_flag__(self): self.calculated_async_flag = True return self From 69347265e82aa7e53461d2015d1b30b12c0f3235 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 02:48:38 +0300 Subject: [PATCH 85/86] make validator nature clear --- chatsky/pipeline/service/extra.py | 2 +- chatsky/pipeline/service/group.py | 2 +- chatsky/pipeline/service/service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chatsky/pipeline/service/extra.py b/chatsky/pipeline/service/extra.py index 1b90e5611..f7475ee58 100644 --- a/chatsky/pipeline/service/extra.py +++ b/chatsky/pipeline/service/extra.py @@ -58,7 +58,7 @@ class ComponentExtraHandler(BaseModel, extra="forbid", arbitrary_types_allowed=T @model_validator(mode="before") @classmethod - def functions_constructor(cls, data: Any): + def functions_validator(cls, data: Any): """ Add support for initializing from a `Callable` or List[`Callable`]. Casts `functions` to `list` if it's not already. diff --git a/chatsky/pipeline/service/group.py b/chatsky/pipeline/service/group.py index 2e5375a0d..8c2efc37a 100644 --- a/chatsky/pipeline/service/group.py +++ b/chatsky/pipeline/service/group.py @@ -66,7 +66,7 @@ class ServiceGroup(PipelineComponent): @model_validator(mode="before") @classmethod - def __components_constructor(cls, data: Any): + def components_validator(cls, data: Any): """ Add support for initializing from a `Callable`, `List` and :py:class:`~.PipelineComponent` (such as :py:class:`~.Service`) diff --git a/chatsky/pipeline/service/service.py b/chatsky/pipeline/service/service.py index 3c661af55..1796ae6b3 100644 --- a/chatsky/pipeline/service/service.py +++ b/chatsky/pipeline/service/service.py @@ -57,7 +57,7 @@ class Service(PipelineComponent): @model_validator(mode="before") @classmethod - def __handler_constructor(cls, data: Any): + def handler_validator(cls, data: Any): """ Add support for initializing from a `Callable`. """ From fccfc5b8889d5ba0a2531cf89989b4edd0807ba3 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 19 Aug 2024 02:48:47 +0300 Subject: [PATCH 86/86] small doc change --- chatsky/pipeline/pipeline/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatsky/pipeline/pipeline/component.py b/chatsky/pipeline/pipeline/component.py index 4af186b05..362530432 100644 --- a/chatsky/pipeline/pipeline/component.py +++ b/chatsky/pipeline/pipeline/component.py @@ -76,7 +76,7 @@ class PipelineComponent(abc.ABC, BaseModel, extra="forbid", arbitrary_types_allo name: Optional[str] = None """ Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), - should not be blank or contain `.` symbol. + should not be blank or contain the ``.`` character. """ path: Optional[str] = None """