4. Conditions and paths#
This tutorial explains how a unique path is generated for each component and how to add a condition for component execution.
API ref for service conditions
[1]:
# installing dependencies
%pip install -q chatsky
Note: you may need to restart the kernel to use updated packages.
[2]:
import logging
import sys
from importlib import reload
from chatsky.conditions import Not, All, ServiceFinished
from chatsky.core.service import Service, ServiceGroup
from chatsky import Pipeline, Context, AnyCondition
from chatsky.utils.testing.common import (
check_happy_path,
is_interactive_mode,
)
from chatsky.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT_KWARGS
reload(logging)
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="")
logger = logging.getLogger(__name__)
Component paths#
Each component has a unique path that can be used as an ID for that component.
A path of a component is a “.” concatenation of names of service groups this component is in and the name of this component.
For example, if component “c” is in group “b” which itself is in group “a” then path of “c” is “a.b.c”.
Naming services
When choosing a name for a component, keep in mind that it should be unique among other components in the same service group.
Computed names#
If a component does not have a name, one is provided to it:
Each component has
computed_name
property which returns a default name. All service groups have “service_group” as their computed name. For services the computed name is the name of the handler or of the service class if handler is not present.E.g. A service with handler
my_func
has computed name “my_func”; and if serviceMyService
is a subclass ofService
and does not definehandler
its computed name is “MyService”.If there are two or more components in a service group with the same computed name, they are assigned names with an incrementing postfix. For example, if there are two service groups with no name in pre-services they will be named “service_group#0” and “service_group#1”.
Names of basic components#
Main component (pipeline) has an empty name;
Name of the pre services group is “pre”;
Name of the post services group is “post”;
Name of the actor service is “actor”.
For example, if you have pre_services=[my_func]
, the full path of the my_func
component is “.pre.my_func”.
Component start condition#
Any component (service or service group) can have a start_condition
.
Start condition is a BaseCondition
that determines whether the component should be executed.
For more information about conditions, see the condition tutorial.
Component status#
At any time each component has a certain status:
NOT_RUN
- Component hasn’t bee executed yet or start condition returned False.RUNNING
- Component is currently being executed.FINISHED
- Component finished successfully.FAILED
- Component execution failed.
For more information, see service_states.
ServiceFinished condition#
ServiceFinished
is a condition that returns True
if another service has the FINISHED
status.
ServiceFinished
accepts the following constructor parameters:
path
(required) - a path of another component.wait
- whether it should wait for the component to complete. This means that the component status cannot beNOT_RUN
orRUNNING
at the time of the check. Defaults toFalse
.
Warning!
It is possible for the pipeline to get stuck in infinite waiting with the ServiceFinished
condition.
Either disable wait
or set a timeout for the service.
For more information about ServiceFinished
, see API ref.
Code explanation#
In this example, two conditionally executed services are illustrated.
The service named running_service
is executed only if both SimpleServices
in pre service group have finished successfully.
never_running_service
is executed only if running_service
is not finished, which should never happen.
Lastly, context_printing_service
prints pipeline runtime information, that contains execution state of all previously run services.
[3]:
class SimpleService(Service):
async def call(self, _: Context):
logger.info(f"Service '{self.name}' is running...")
class NeverRunningService(Service):
async def call(self, _: Context):
raise Exception(f"Oh no! The '{self.name}' service is running!")
start_condition: AnyCondition = Not(
ServiceFinished(".post.named_group.running_service", wait=True)
)
class RuntimeInfoPrintingService(Service):
async def call(self, _: Context):
logger.info(
f"Service '{self.name}' runtime execution info:"
f"{self.model_dump_json(indent=4)}"
)
[4]:
pipeline = Pipeline(
**TOY_SCRIPT_KWARGS,
pre_services=[
SimpleService(),
# This service will be named "SimpleService#0"
SimpleService(),
# This service will be named "SimpleService#1"
],
# this group is named "pre"
post_services=[
ServiceGroup(
name="named_group",
components=[
SimpleService(
start_condition=All(
ServiceFinished(".pre.SimpleService#0"),
ServiceFinished(".pre.SimpleService#1"),
),
# Alternative:
# ServiceFinished(".pre")
name="running_service",
),
# This simple service is named "running_service"
NeverRunningService(),
# this service will be named "NeverRunningService"
],
fully_concurrent=True,
# Makes components in the group run asynchronously,
# unless one is waiting for another to complete,
# which is what happens with NeverRunningService.
),
RuntimeInfoPrintingService(),
],
)
[5]:
if __name__ == "__main__":
check_happy_path(pipeline, HAPPY_PATH[:1], printout=True)
if is_interactive_mode():
pipeline.run()
USER: text='Hi'
Service 'SimpleService#0' is running...
Service 'SimpleService#1' is running...
Service 'running_service' is running...
Service 'RuntimeInfoPrintingService' runtime execution info:{
"before_handler": {
"functions": [],
"timeout": null,
"concurrent": false
},
"after_handler": {
"functions": [],
"timeout": null,
"concurrent": false
},
"timeout": null,
"concurrent": false,
"start_condition": {
"root": true
},
"name": "RuntimeInfoPrintingService",
"path": ".post.RuntimeInfoPrintingService",
"handler": null
}
BOT : text='Hi, how are you?'