From 92271a54fdd4ad09cb30733026604cb5b65e88ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:30:08 +0300 Subject: [PATCH 01/22] build(deps-dev): bump pydata-sphinx-theme from 0.13.3 to 0.14.1 (#232) Bumps [pydata-sphinx-theme](https://github.com/pydata/pydata-sphinx-theme) from 0.13.3 to 0.14.1. - [Release notes](https://github.com/pydata/pydata-sphinx-theme/releases) - [Commits](https://github.com/pydata/pydata-sphinx-theme/compare/v0.13.3...v0.14.1) --- updated-dependencies: - dependency-name: pydata-sphinx-theme dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 010f0ba89..a23817672 100644 --- a/setup.py +++ b/setup.py @@ -160,7 +160,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: doc = merge_req_lists( [ "sphinx==7.2.2", - "pydata-sphinx-theme==0.13.3", + "pydata-sphinx-theme==0.14.1", "sphinxcontrib-apidoc==0.4.0", "sphinxcontrib-httpdomain==1.8.0", "sphinxcontrib-katex==0.9.0", From 907c4de1cf19a1825810f1e09711e09cfbc8c2f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:30:22 +0300 Subject: [PATCH 02/22] build(deps-dev): bump flask[async] from 2.3.2 to 3.0.0 (#240) Bumps [flask[async]](https://github.com/pallets/flask) from 2.3.2 to 3.0.0. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/2.3.2...3.0.0) --- updated-dependencies: - dependency-name: flask[async] dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a23817672..65a800242 100644 --- a/setup.py +++ b/setup.py @@ -140,7 +140,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: ) tutorial_dependencies = [ - "flask[async]==2.3.2", + "flask[async]==3.0.0", "psutil==5.9.5", "telethon==1.30.0", "fastapi==0.103.1", From 697bda6b5e6775290cdd39fa4300503e6540394d Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 16 Oct 2023 23:31:18 +0300 Subject: [PATCH 03/22] Delete tests/stats/test_patch.py (#243) --- tests/stats/test_patch.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 tests/stats/test_patch.py diff --git a/tests/stats/test_patch.py b/tests/stats/test_patch.py deleted file mode 100644 index 284ea0b69..000000000 --- a/tests/stats/test_patch.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest - -try: - from dff import stats # noqa: F401 - from opentelemetry.proto.common.v1.common_pb2 import AnyValue - from opentelemetry.exporter.otlp.proto.grpc.exporter import _translate_value -except ImportError: - pytest.skip(allow_module_level=True, reason="One of the Opentelemetry packages is missing.") - - -@pytest.mark.parametrize( - ["value", "expected_field"], [(1, "int_value"), ({"a": "b"}, "kvlist_value"), (None, "string_value")] -) -def test_body_translation(value, expected_field): - assert _translate_value.__wrapped__.__name__ == "_translate_value" - translated_value = _translate_value(value) - assert isinstance(translated_value, AnyValue) - assert translated_value.IsInitialized() - assert getattr(translated_value, expected_field, None) is not None From 441899cfab2e028abc410bed89785417fe69b370 Mon Sep 17 00:00:00 2001 From: Alexander Sergeev Date: Mon, 16 Oct 2023 22:31:44 +0200 Subject: [PATCH 04/22] Doclinks added to contribution guide (#245) * doclinks added to contribution guide * minor fixes --------- Co-authored-by: Roman Zlobin --- CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdbe7baca..eacd85ea4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,26 @@ WARNING! Because of the current patching solution, `make doc` modifies some of t so it is strongly advised to use it carefully and in virtual environment only. However, this behavior is likely to be changed in the future. +#### Documentation links + +In your tutorials, you can use special expanding directives in markdown cells. +They can help shorten the comments and avoid boilerplate code. +The documentation links generated by the directives are always relative +to the local documentation and verified during build. + +- `%pip install {args}` + This directive generates dependency installation cell, adds a comment and sets up "quiet" flag. + + It should be used in tutorials, like this: `# %pip install dff[...]`. +- `%doclink({args})` + This directive generates a documentation link. It supports 2 or three arguments and the generated link will look like: `ARG1/ARG2#ARG3`. + + The first argument can be either `api` for DFF codebase, `tutorial` for tutorials or `guide` for user guides. +- `%mddoclink({args})` + This directive is a shortcut for `%doclink` that generates a markdown format link instead. + + The generated link will be either `[ARG2](%doclink(ARG1,ARG2))` or `[ARG3](%doclink(ARG1,ARG2,ARG3))`. + ### Style For style supporting we propose `black`, which is a PEP 8 compliant opinionated formatter. It doesn't take previous formatting into account. See more about [black](https://github.com/psf/black). From f8ec1d3de64c606aba3a49401117c6d09268f561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:32:55 +0300 Subject: [PATCH 05/22] build(deps-dev): bump locust from 2.16.1 to 2.17.0 (#247) Bumps [locust](https://github.com/locustio/locust) from 2.16.1 to 2.17.0. - [Release notes](https://github.com/locustio/locust/releases) - [Changelog](https://github.com/locustio/locust/blob/master/CHANGELOG.md) - [Commits](https://github.com/locustio/locust/compare/2.16.1...2.17.0) --- updated-dependencies: - dependency-name: locust dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 65a800242..ad034934b 100644 --- a/setup.py +++ b/setup.py @@ -146,7 +146,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: "fastapi==0.103.1", "uvicorn==0.23.1", "websockets==11.0.2", - "locust==2.16.1", + "locust==2.17.0", "streamlit==1.27.0", "streamlit-chat==0.1.1", ] From 0cebfacf9c4d9ca0c2958f617a24687381dc7390 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:33:16 +0300 Subject: [PATCH 06/22] build(deps-dev): bump mypy from 1.5.0 to 1.6.0 (#248) Bumps [mypy](https://github.com/python/mypy) from 1.5.0 to 1.6.0. - [Commits](https://github.com/python/mypy/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad034934b..af2006868 100644 --- a/setup.py +++ b/setup.py @@ -182,7 +182,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: ] mypy_dependencies = [ - "mypy==1.5.0", + "mypy==1.6.0", ] devel_full = merge_req_lists( From dd85149d473f4e16112464b7b38258d8ea75b154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:33:57 +0300 Subject: [PATCH 07/22] build(deps-dev): bump telethon from 1.30.0 to 1.31.0 (#251) Bumps [telethon](https://github.com/LonamiWebs/Telethon) from 1.30.0 to 1.31.0. - [Release notes](https://github.com/LonamiWebs/Telethon/releases) - [Commits](https://github.com/LonamiWebs/Telethon/compare/v1.30.0...v1.31.0) --- updated-dependencies: - dependency-name: telethon dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index af2006868..2ba6651a6 100644 --- a/setup.py +++ b/setup.py @@ -142,7 +142,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: tutorial_dependencies = [ "flask[async]==3.0.0", "psutil==5.9.5", - "telethon==1.30.0", + "telethon==1.31.0", "fastapi==0.103.1", "uvicorn==0.23.1", "websockets==11.0.2", From 8c79188be0b0b3b84b63417134f2b592d1d0edfb Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 16 Oct 2023 23:34:23 +0300 Subject: [PATCH 08/22] add member-order and exclude-members (#249) --- docs/source/conf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index f2e314ceb..ff1c47525 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -146,7 +146,13 @@ ] -autodoc_default_options = {"members": True, "undoc-members": False, "private-members": True} +autodoc_default_options = { + "members": True, + "undoc-members": False, + "private-members": True, + "member-order": "bysource", + "exclude-members": "_abc_impl, model_fields", +} def setup(_): From 742bf7141d27e606becbe7672e74bf93947fe152 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Mon, 16 Oct 2023 23:34:55 +0300 Subject: [PATCH 09/22] account for `_full` postfix when sorting tutorials (#250) --- docs/source/utils/generate_tutorials.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/source/utils/generate_tutorials.py b/docs/source/utils/generate_tutorials.py index 1f74ca1a4..5e8e0e333 100644 --- a/docs/source/utils/generate_tutorials.py +++ b/docs/source/utils/generate_tutorials.py @@ -68,7 +68,16 @@ def sort_tutorial_file_tree(files: Set[Path]) -> List[Path]: :param files: Files list to sort. """ tutorials = {file for file in files if file.stem.split("_")[0].isdigit()} - return sorted(tutorials, key=lambda file: int(file.stem.split("_")[0])) + sorted(files - tutorials) + + def sort_key(tutorial_file_name: Path) -> float: + tutorial_number = float(tutorial_file_name.stem.split("_")[0]) + + # full tutorials should go after tutorials with the same number + if tutorial_file_name.stem.endswith("_full"): + return tutorial_number + 0.5 + return tutorial_number + + return sorted(tutorials, key=sort_key) + sorted(files - tutorials) def iterate_tutorials_dir_generating_links(source: Path, dest: Path, base: str) -> List[Path]: From 2bbead7e8b000f3351faa73eef72ee9e73310ad0 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Mon, 16 Oct 2023 23:54:34 +0300 Subject: [PATCH 10/22] Add context guide (#244) * add stub section * update guide * add attribute and method information * add code-block to introduction * update context guide * update references * Update field descriptions * remove unused function in a referenced tutorial * fix context doc * add links to the context guide in the basic guide * fix context guide Notable changes: - make links anonymous - Replace misc function - Add more information about fields * add context storage section * remove comments about to-be-deprecated methods * merge attributes and methods into one section * fixes to the context storage section --------- Co-authored-by: Roman Zlobin --- dff/script/core/context.py | 97 ++++--- docs/source/user_guides.rst | 7 + docs/source/user_guides/basic_conceptions.rst | 8 +- docs/source/user_guides/context_guide.rst | 247 ++++++++++++++++++ .../script/core/7_pre_response_processing.py | 7 - 5 files changed, 312 insertions(+), 54 deletions(-) create mode 100644 docs/source/user_guides/context_guide.rst diff --git a/dff/script/core/context.py b/dff/script/core/context.py index 6658c346f..78ee18072 100644 --- a/dff/script/core/context.py +++ b/dff/script/core/context.py @@ -32,7 +32,7 @@ def get_last_index(dictionary: dict) -> int: """ - Obtaining the last index from the `dictionary`. Functions returns `-1` if the `dict` is empty. + Obtain the last index from the `dictionary`. Return `-1` if the `dict` is empty. :param dictionary: Dictionary with unsorted keys. :return: Last index from the `dictionary`. @@ -44,6 +44,9 @@ def get_last_index(dictionary: dict) -> int: class Context(BaseModel): """ A structure that is used to store data about the context of a dialog. + + Avoid storing unserializable data in the fields of this class in order for + context storages to work. """ id: Union[UUID, int, str] = Field(default_factory=uuid4) @@ -77,13 +80,15 @@ class Context(BaseModel): `misc` stores any custom data. The scripting doesn't use this dictionary by default, so storage of any data won't reflect on the work on the internal Dialog Flow Scripting functions. + Avoid storing unserializable data in order for context storages to work. + - key - Arbitrary data name. - value - Arbitrary data. """ validation: bool = False """ - `validation` is a flag that signals that :py:class:`~dff.script.Pipeline`, - while being initialized, checks the :py:class:`~dff.script.Script`. + `validation` is a flag that signals that :py:class:`~dff.pipeline.pipeline.pipeline.Pipeline`, + while being initialized, checks the :py:class:`~dff.script.core.script.Script`. The functions that can give not valid data while being validated must use this flag to take the validation mode into account. Otherwise the validation will not be passed. @@ -91,12 +96,12 @@ class Context(BaseModel): framework_states: Dict[ModuleName, Dict[str, Any]] = {} """ `framework_states` is used for addons states or for - :py:class:`~dff.script.Pipeline`'s states. - :py:class:`~dff.script.Pipeline` + :py:class:`~dff.pipeline.pipeline.pipeline.Pipeline`'s states. + :py:class:`~dff.pipeline.pipeline.pipeline.Pipeline` records all its intermediate conditions into the `framework_states`. - After :py:class:`~dff.script.Context` processing is finished, - :py:class:`~dff.script.Pipeline` resets `framework_states` and - returns :py:class:`~dff.script.Context`. + After :py:class:`~.Context` processing is finished, + :py:class:`~dff.pipeline.pipeline.pipeline.Pipeline` resets `framework_states` and + returns :py:class:`~.Context`. - key - Temporary variable name. - value - Temporary variable data. @@ -106,7 +111,7 @@ class Context(BaseModel): @classmethod def sort_dict_keys(cls, dictionary: dict) -> dict: """ - Sorting the keys in the `dictionary`. This needs to be done after deserialization, + Sort the keys in the `dictionary`. This needs to be done after deserialization, since the keys are deserialized in a random order. :param dictionary: Dictionary with unsorted keys. @@ -117,16 +122,15 @@ def sort_dict_keys(cls, dictionary: dict) -> dict: @classmethod def cast(cls, ctx: Optional[Union["Context", dict, str]] = None, *args, **kwargs) -> "Context": """ - Transforms different data types to the objects of - :py:class:`~dff.script.Context` class. - Returns an object of :py:class:`~dff.script.Context` + Transform different data types to the objects of the + :py:class:`~.Context` class. + Return an object of the :py:class:`~.Context` type that is initialized by the input data. - :param ctx: Different data types, that are used to initialize object of - :py:class:`~dff.script.Context` type. - The empty object of :py:class:`~dff.script.Context` - type is created if no data are given. - :return: Object of :py:class:`~dff.script.Context` + :param ctx: Data that is used to initialize an object of the + :py:class:`~.Context` type. + An empty :py:class:`~.Context` object is returned if no data is given. + :return: Object of the :py:class:`~.Context` type that is initialized by the input data. """ if not ctx: @@ -137,14 +141,15 @@ def cast(cls, ctx: Optional[Union["Context", dict, str]] = None, *args, **kwargs ctx = Context.model_validate_json(ctx) elif not issubclass(type(ctx), Context): raise ValueError( - f"context expected as sub class of Context class or object of dict/str(json) type, but got {ctx}" + f"Context expected to be an instance of the Context class " + f"or an instance of the dict/str(json) type. Got: {type(ctx)}" ) return ctx def add_request(self, request: Message): """ - Adds to the context the next `request` corresponding to the next turn. - The addition takes place in the `requests` and `new_index = last_index + 1`. + Add a new `request` to the context. + The new `request` is added with the index of `last_index + 1`. :param request: `request` to be added to the context. """ @@ -154,8 +159,8 @@ def add_request(self, request: Message): def add_response(self, response: Message): """ - Adds to the context the next `response` corresponding to the next turn. - The addition takes place in the `responses`, and `new_index = last_index + 1`. + Add a new `response` to the context. + The new `response` is added with the index of `last_index + 1`. :param response: `response` to be added to the context. """ @@ -165,9 +170,8 @@ def add_response(self, response: Message): def add_label(self, label: NodeLabel2Type): """ - Adds to the context the next :py:const:`label `, - corresponding to the next turn. - The addition takes place in the `labels`, and `new_index = last_index + 1`. + Add a new :py:data:`~.NodeLabel2Type` to the context. + The new `label` is added with the index of `last_index + 1`. :param label: `label` that we need to add to the context. """ @@ -180,12 +184,12 @@ def clear( field_names: Union[Set[str], List[str]] = {"requests", "responses", "labels"}, ): """ - Deletes all recordings from the `requests`/`responses`/`labels` except for + Delete all records from the `requests`/`responses`/`labels` except for the last `hold_last_n_indices` turns. If `field_names` contains `misc` field, `misc` field is fully cleared. - :param hold_last_n_indices: Number of last turns that remain under clearing. - :param field_names: Properties of :py:class:`~dff.script.Context` we need to clear. + :param hold_last_n_indices: Number of last turns to keep. + :param field_names: Properties of :py:class:`~.Context` to clear. Defaults to {"requests", "responses", "labels"} """ field_names = field_names if isinstance(field_names, set) else set(field_names) @@ -206,9 +210,12 @@ def clear( @property def last_label(self) -> Optional[NodeLabel2Type]: """ - Returns the last :py:const:`~dff.script.NodeLabel2Type` of - the :py:class:`~dff.script.Context`. - Returns `None` if `labels` is empty. + Return the last :py:data:`~.NodeLabel2Type` of + the :py:class:`~.Context`. + Return `None` if `labels` is empty. + + Since `start_label` is not added to the `labels` field, + empty `labels` usually indicates that the current node is the `start_node`. """ last_index = get_last_index(self.labels) return self.labels.get(last_index) @@ -216,8 +223,8 @@ def last_label(self) -> Optional[NodeLabel2Type]: @property def last_response(self) -> Optional[Message]: """ - Returns the last `response` of the current :py:class:`~dff.script.Context`. - Returns `None` if `responses` is empty. + Return the last `response` of the current :py:class:`~.Context`. + Return `None` if `responses` is empty. """ last_index = get_last_index(self.responses) return self.responses.get(last_index) @@ -225,7 +232,7 @@ def last_response(self) -> Optional[Message]: @last_response.setter def last_response(self, response: Optional[Message]): """ - Sets the last `response` of the current :py:class:`~dff.core.engine.core.context.Context`. + Set the last `response` of the current :py:class:`~.Context`. Required for use with various response wrappers. """ last_index = get_last_index(self.responses) @@ -234,8 +241,8 @@ def last_response(self, response: Optional[Message]): @property def last_request(self) -> Optional[Message]: """ - Returns the last `request` of the current :py:class:`~dff.script.Context`. - Returns `None` if `requests` is empty. + Return the last `request` of the current :py:class:`~.Context`. + Return `None` if `requests` is empty. """ last_index = get_last_index(self.requests) return self.requests.get(last_index) @@ -243,7 +250,7 @@ def last_request(self) -> Optional[Message]: @last_request.setter def last_request(self, request: Optional[Message]): """ - Sets the last `request` of the current :py:class:`~dff.core.engine.core.context.Context`. + Set the last `request` of the current :py:class:`~.Context`. Required for use with various request wrappers. """ last_index = get_last_index(self.requests) @@ -252,7 +259,7 @@ def last_request(self, request: Optional[Message]): @property def current_node(self) -> Optional[Node]: """ - Returns current :py:class:`~dff.script.Node`. + Return current :py:class:`~dff.script.core.script.Node`. """ actor = self.framework_states.get("actor", {}) node = ( @@ -264,17 +271,21 @@ def current_node(self) -> Optional[Node]: ) if node is None: logger.warning( - "The `current_node` exists when an actor is running between `ActorStage.GET_PREVIOUS_NODE`" - " and `ActorStage.FINISH_TURN`" + "The `current_node` method should be called " + "when an actor is running between the " + "`ActorStage.GET_PREVIOUS_NODE` and `ActorStage.FINISH_TURN` stages." ) return node def overwrite_current_node_in_processing(self, processed_node: Node): """ - Overwrites the current node with a processed node. This method only works in processing functions. + Set the current node to be `processed_node`. + This method only works in processing functions (pre-response and pre-transition). + + The actual current node is not changed. - :param processed_node: `node` that we need to overwrite current node. + :param processed_node: `node` to set as the current node. """ is_processing = self.framework_states.get("actor", {}).get("processed_node") if is_processing: @@ -282,7 +293,7 @@ def overwrite_current_node_in_processing(self, processed_node: Node): else: logger.warning( f"The `{self.overwrite_current_node_in_processing.__name__}` " - "function can only be run during processing functions." + "method can only be called from processing functions (either pre-response or pre-transition)." ) diff --git a/docs/source/user_guides.rst b/docs/source/user_guides.rst index 8724a1489..fa274db88 100644 --- a/docs/source/user_guides.rst +++ b/docs/source/user_guides.rst @@ -9,6 +9,12 @@ those include but are not limited to: dialog graph creation, specifying start an setting transitions and conditions, using ``Context`` object in order to receive information about current script execution. +:doc:`Context guide <./user_guides/context_guide>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``context guide`` walks you through the details of working with the +``Context`` object, the backbone of the DFF API, including most of the relevant fields and methods. + :doc:`Superset guide <./user_guides/superset_guide>` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -22,4 +28,5 @@ Superset dashboard shipped with DFF. :hidden: user_guides/basic_conceptions + user_guides/context_guide user_guides/superset_guide diff --git a/docs/source/user_guides/basic_conceptions.rst b/docs/source/user_guides/basic_conceptions.rst index 9f81a8610..6cd4d2964 100644 --- a/docs/source/user_guides/basic_conceptions.rst +++ b/docs/source/user_guides/basic_conceptions.rst @@ -241,7 +241,7 @@ That's what we've changed: .. note:: - See `documentation of Context object`_. + See `guide on Context objects`_. * Transitions were changed: transitions to next, previous and current node were replaced with special standard transitions. @@ -268,7 +268,7 @@ For example: * You can serialize context (available on every transition and response) to json or dictionary in order to debug it or extract some values. - See `tutorial on context serialization`_. + See `guide on context serialization`_. * You can alter user input and modify generated responses. User input can be altered with ``PRE_RESPONSE_PROCESSING`` and will happen **before** response generation. @@ -293,11 +293,11 @@ Happy building! .. _tutorial on basic dialog structure: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.1_basics.html .. _tutorial on response functions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.3_responses.html -.. _documentation of Context object: https://deeppavlov.github.io/dialog_flow_framework/apiref/dff.script.core.context.html +.. _guide on Context objects: ../user_guides/context_guide.html .. _tutorial on transitions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.4_transitions.html .. _tutorial on conditions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.2_conditions.html .. _tutorial on global transitions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.5_global_transitions.html -.. _tutorial on context serialization: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.6_context_serialization.html +.. _guide on context serialization: ../user_guides/context_guide.html#serialization .. _tutorial on pre-response processing: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.7_pre_response_processing.html .. _tutorial on pre-transition processing: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.9_pre_transitions_processing.html .. _tutorial on script MISC: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.8_misc.html diff --git a/docs/source/user_guides/context_guide.rst b/docs/source/user_guides/context_guide.rst new file mode 100644 index 000000000..1dc47712c --- /dev/null +++ b/docs/source/user_guides/context_guide.rst @@ -0,0 +1,247 @@ +Context guide +-------------- + +Introduction +~~~~~~~~~~~~ + +The ``Context`` class is a backbone component of the DFF API. +Like the name suggests, this data structure is used to store information +about the current state, or context, of a particular conversation. +Each individual user has their own ``Context`` instance and can be identified by it. + +``Context`` is used to keep track of the user's requests, bot's replies, +user-related and request-related annotations, and any other information +that is relevant to the conversation with the user. + +.. note:: + + Since most callback functions used in DFF script and DFF pipeline (see the `basic guide <./basic_conceptions.rst>`__) + need to either read or update the current dialog state, + the framework-level convention is that all functions of this kind + use ``Context`` as their first parameter. This dependency is being + injected by the pipeline during its run. + Thus, understanding the ``Context`` class is essential for developing custom conversation logic + which is mostly made up by the said functions. + +As a callback parameter, ``Context`` provides a convenient interface for working with data, +allowing developers to easily add, retrieve, +and manipulate data as the conversation progresses. + +Let's consider some of the built-in callback instances to see how the context can be leveraged: + +.. code-block:: python + :linenos: + + pattern = re.compile("[a-zA-Z]+") + + def regexp_condition_handler( + ctx: Context, pipeline: Pipeline, *args, **kwargs + ) -> bool: + # retrieve the current request + request = ctx.last_request + if request.text is None: + return False + return bool(pattern.search(request.text)) + +The code above is a condition function (see the `basic guide <./basic_conceptions.rst>`__) +that belongs to the ``TRANSITIONS`` section of the script and returns `True` or `False` +depending on whether the current user request matches the given pattern. +As can be seen from the code block, the current +request (``last_request``) can be easily retrieved as one of the attributes of the ``Context`` object. +Likewise, the ``last_response`` (bot's current reply) or the ``last_label`` +(the name of the currently visited node) attributes can be used in the same manner. + +Another common use case is leveraging the ``misc`` field (see below for a detailed description): +pipeline functions or ``PROCESSING`` callbacks can write arbitrary values to the misc field, +making those available for other context-dependent functions. + +.. code-block:: python + :linenos: + + import urllib.request + import urllib.error + + def ping_example_com( + ctx: Context, *_, **__ + ): + try: + with urllib.request.urlopen("https://example.com/") as webpage: + web_content = webpage.read().decode( + webpage.headers.get_content_charset() + ) + result = "Example Domain" in web_content + except urllib.error.URLError: + result = False + ctx.misc["can_ping_example_com"] = result + +.. + todo: link to the user defined functions tutorial + + .. note:: + For more information about user-defined functions see the `user functions guide <./user_functions.rst>`__. + +API +~~~ + +This sections describes the API of the ``Context`` class. + +For more information, such as method signatures, see +`API reference <./apiref/dff.script.core.context.html#dff.script.core.context.Context>`__. + +Attributes +========== + +* **id**: This attribute represents the unique context identifier. By default, it is randomly generated using uuid4. + In most cases, this attribute will be used to identify a user. + +* **labels**: The labels attribute stores the history of all passed labels within the conversation. + It maps turn IDs to labels. The collection is ordered, so getting the last item of the mapping + always shows the last visited node. + + Note that `labels` only stores the nodes that were transitioned to + so `start_label` will not be in this attribute. + +* **requests**: The requests attribute maintains the history of all received requests by the agent. + It also maps turn IDs to requests. Like labels, it stores the requests in-order. + +* **responses**: This attribute keeps a record of all agent responses, mapping turn IDs to responses. + Stores the responses in-order. + +* **misc**: The misc attribute is a dictionary for storing custom data. This field is not used by any of the + built-in DFF classes or functions, so the values that you write there are guaranteed to persist + throughout the lifetime of the ``Context`` object. + +* **framework_states**: This attribute is used for storing addon or pipeline states. + Each turn, the DFF pipeline records the intermediary states of its components into this field, + and clears it at the end of the turn. For this reason, developers are discouraged from storing + their own data in this field. + +Methods +======= + +The methods of the ``Context`` class can be divided into two categories: + +* Public methods that get called manually in custom callbacks and in functions that depend on the context. +* Methods that are not designed for manual calls and get called automatically during pipeline runs, + i.e. quasi-private methods. You may still need them when developing extensions or heavily modifying DFF. + +Public methods +^^^^^^^^^^^^^^ + +* **last_request**: Return the last request of the context, or `None` if the ``requests`` field is empty. + + Note that a request is added right after the context is created/retrieved from db, + so an empty ``requests`` field usually indicates an issue with the messenger interface. + +* **last_response**: Return the last response of the context, or `None` if the ``responses`` field is empty. + + Responses are added at the end of each turn, so an empty ``response`` field is something you should definitely consider. + +* **last_label**: Return the last label of the context, or `None` if the ``labels`` field is empty. + Last label is always the name of the current node but not vice versa: + + Since ``start_label`` is not added to the ``labels`` field, + empty ``labels`` usually indicates that the current node is the `start_node`. + After a transition is made from the `start_node` + the label of that transition is added to the field. + +* **clear**: Clear all items from context fields, optionally keeping the data from `hold_last_n_indices` turns. + You can specify which fields to clear using the `field_names` parameter. This method is designed for cases + when contexts are shared over high latency networks. + +.. note:: + + See the `preprocessing tutorial <../tutorials/tutorials.script.core.7_pre_response_processing.py>`__. + +Private methods +^^^^^^^^^^^^^^^ + +* **set_last_response, set_last_request**: These methods allow you to set the last response or request for the current context. + This functionality can prove useful if you want to create a middleware component that overrides the pipeline functionality. + +* **add_request**: Add a request to the context. + It updates the `requests` dictionary. This method is called by the `Pipeline` component + before any of the `pipeline services <../tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.py>`__ are executed, + including `Actor <../apiref/dff.pipeline.pipeline.actor.html>`__. + +* **add_response**: Add a response to the context. + It updates the `responses` dictionary. This function is run by the `Actor <../apiref/dff.pipeline.pipeline.actor.html>`__ pipeline component at the end of the turn, after it has run + the `PRE_RESPONSE_PROCESSING <../tutorials/tutorials.script.core.7_pre_response_processing.py>`__ functions. + + To be more precise, this method is called between the ``CREATE_RESPONSE`` and ``FINISH_TURN`` stages. + For more information about stages, see `ActorStages <../apiref/dff.script.core.types.html#dff.script.core.types.ActorStage>`__. + +* **add_label**: Add a label to the context. + It updates the `labels` field. This method is called by the `Actor <../apiref/dff.pipeline.pipeline.actor.html>`_ component when transition conditions + have been resolved, and when `PRE_TRANSITIONS_PROCESSING <../tutorials/tutorials.script.core.9_pre_transitions_processing.py>`__ callbacks have been run. + + To be more precise, this method is called between the ``GET_NEXT_NODE`` and ``REWRITE_NEXT_NODE`` stages. + For more information about stages, see `ActorStages <../apiref/dff.script.core.types.html#dff.script.core.types.ActorStage>`__. + +* **current_node**: Return the current node of the context. This is particularly useful for tracking the node during the conversation flow. + This method only returns a node inside ``PROCESSING`` callbacks yielding ``None`` in other contexts. + +Context storages +~~~~~~~~~~~~~~~~ + +Since context instances contain all the information, relevant for a particular user, there needs to be a way +to persistently store that information and to make it accessible in different user sessions. +This functionality is implemented by the ``context storages`` module that provides +the uniform ``DBContextStorage`` interface as well as child classes thereof that integrate +various database types (see the +`api reference <../apiref/dff.context_storages.database.html#dff.context_storages.database.DBContextStorage>`_). + +The supported storage options are as follows: + +* `JSON `_ +* `pickle `_ +* `shelve `_ +* `SQLite `_ +* `PostgreSQL `_ +* `MySQL `_ +* `MongoDB `_ +* `Redis `_ +* `Yandex DataBase `_ + +``DBContextStorage`` instances can be uniformly constructed using the ``context_storage_factory`` function. +The function's only parameter is a connection string that specifies both the database type +and the connection parameters, for example, *mongodb://admin:pass@localhost:27016/admin*. +(`see the reference <../apiref/dff.context_storages.database.html#dff.context_storages.database.context_storage_factory>`_) + +The GitHub-based distribution of DFF includes Docker images for each of the supported database types. +Therefore, the easiest way to deploy your service together with a database is to clone the GitHub +distribution and to take advantage of the packaged +`docker-compose file `_. + +.. code-block:: shell + :linenos: + + git clone https://github.com/deeppavlov/dialog_flow_framework.git + cd dialog_flow_framework + # assuming we need to deploy mongodb + docker-compose up mongo + +The images can be configured using the docker-compose file or the +`environment file `_, +also available in the distribution. Consult these files for more options. + +.. warning:: + + The data transmission protocols require the data to be JSON-serializable. DFF tackles this problem + through utilization of ``pydantic`` as described in the next section. + +Serialization +~~~~~~~~~~~~~ + +The fact that the ``Context`` class is a Pydantic model makes it easily convertible to other data formats, +such as JSON. For instance, as a developer, you don't need to implement instructions on how datetime fields +need to be marshalled, since this functionality is provided by Pydantic out of the box. +As a result, working with web interfaces and databases that require the transmitted data to be serialized +becomes as easy as calling the `model_dump_json` method: + +.. code-block:: python + + context = Context() + serialized_context = context.model_dump_json() + +Knowing that, you can easily extend DFF to work with storages like Memcache or web APIs of your liking. \ No newline at end of file diff --git a/tutorials/script/core/7_pre_response_processing.py b/tutorials/script/core/7_pre_response_processing.py index d43e9c17d..233d62f75 100644 --- a/tutorials/script/core/7_pre_response_processing.py +++ b/tutorials/script/core/7_pre_response_processing.py @@ -37,13 +37,6 @@ # %% -def add_label_processing(ctx: Context, _: Pipeline, *args, **kwargs) -> Context: - processed_node = ctx.current_node - processed_node.response = Message(text=f"{ctx.last_label}: {processed_node.response.text}") - ctx.overwrite_current_node_in_processing(processed_node) - return ctx - - def add_prefix(prefix): def add_prefix_processing(ctx: Context, _: Pipeline, *args, **kwargs) -> Context: processed_node = ctx.current_node From fb3bbe116ccb5b49a8e715e089eed283bd037f40 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 17 Oct 2023 01:33:05 +0300 Subject: [PATCH 11/22] Add docker profiles (#246) * add docker profiles * update workflows * update CONTRIBUTING.md --- .github/workflows/build_and_publish_docs.yml | 2 +- .github/workflows/test_coverage.yml | 2 +- .github/workflows/test_full.yml | 4 +- CONTRIBUTING.md | 40 +++++++++++++++----- docker-compose.yml | 16 ++++++++ docs/source/user_guides/superset_guide.rst | 2 +- makefile | 2 +- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build_and_publish_docs.yml b/.github/workflows/build_and_publish_docs.yml index 6d43756d2..15bbd6cc3 100644 --- a/.github/workflows/build_and_publish_docs.yml +++ b/.github/workflows/build_and_publish_docs.yml @@ -30,7 +30,7 @@ jobs: - name: Build images run: | - docker-compose up -d + make docker_up - uses: r-lib/actions/setup-pandoc@v2 with: diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml index fae5d408a..8931262b5 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/test_coverage.yml @@ -28,7 +28,7 @@ jobs: - name: Build images run: | - docker-compose up -d + make docker_up - name: set up python 3.10 uses: actions/setup-python@v4 diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index 381c223b0..17005436d 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -31,7 +31,7 @@ jobs: - name: Build images if: matrix.os == 'ubuntu-latest' run: | - docker-compose up -d + make docker_up - name: set up python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -65,7 +65,7 @@ jobs: - name: Build images run: | - docker-compose up -d + make docker_up - name: set up python 3.8 uses: actions/setup-python@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eacd85ea4..f4055262c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,16 +120,36 @@ make format Tests are configured via [`.env_file`](.env_file). ### Docker -For integration tests, DFF uses Docker images of supported databases as well as docker-compose configuration. -The following images are required for complete integration testing: -1. `mysql` -2. `postgres` -3. `redis` -4. `mongo` -5. `cr.yandex/yc/yandex-docker-local-ydb` - -All of them will be downloaded, launched and awaited upon running integration test make command (`make test_all`). -However, they can be downloaded separately with `make docker_up` and awaited with `make wait_db` commands. +DFF uses docker images for two purposes: +1. Database images for integration testing. +2. Images for statistics collection. + +The first group can be launched via + +```bash +docker-compose --profile context_storage up +``` + +This will download and run all the databases (`mysql`, `postgres`, `redis`, `mongo`, `ydb`). + +The second group can be launched via + +```bash +docker-compose --profile stats up +``` + +This will download and launch Superset Dashboard, Clickhouse, OpenTelemetry Collector. + +To launch both groups run +```bash +docker-compose --profile context_storage --profile stats up +``` +or +```bash +make docker_up +``` + +This will be done automatically when running `make test_all`. ### Other provided features You can get more info about `make` commands by `help`: diff --git a/docker-compose.yml b/docker-compose.yml index 9c33c632a..382dc1dea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,18 +3,24 @@ services: mysql: env_file: [.env_file] image: mysql:latest + profiles: + - context_storage restart: unless-stopped ports: - 3307:3306 psql: env_file: [.env_file] image: postgres:latest + profiles: + - context_storage restart: unless-stopped ports: - 5432:5432 redis: env_file: [.env_file] image: redis:latest + profiles: + - context_storage restart: unless-stopped command: --requirepass pass ports: @@ -22,12 +28,16 @@ services: mongo: env_file: [.env_file] image: mongo:latest + profiles: + - context_storage restart: unless-stopped ports: - 27017:27017 ydb: env_file: [.env_file] image: cr.yandex/yc/yandex-docker-local-ydb:latest + profiles: + - context_storage restart: unless-stopped ports: - 8765:8765 @@ -40,11 +50,15 @@ services: context: ./dff/utils/docker dockerfile: dockerfile_stats image: ghcr.io/deeppavlov/superset_df_dashboard:latest + profiles: + - stats ports: - "8088:8088" clickhouse: env_file: [.env_file] image: clickhouse/clickhouse-server:latest + profiles: + - stats restart: unless-stopped ports: - '8123:8123' @@ -54,6 +68,8 @@ services: - ch-data:/var/lib/clickhouse/ otelcol: image: otel/opentelemetry-collector-contrib:latest + profiles: + - stats container_name: otel-col restart: unless-stopped command: [ "--config=/etc/otelcol-config.yml", "--config=/etc/otelcol-config-extras.yml" ] diff --git a/docs/source/user_guides/superset_guide.rst b/docs/source/user_guides/superset_guide.rst index 5add23bde..c19142055 100644 --- a/docs/source/user_guides/superset_guide.rst +++ b/docs/source/user_guides/superset_guide.rst @@ -31,7 +31,7 @@ Collection procedure git clone https://github.com/deeppavlov/dialog_flow_framework.git # launch the required services cd dialog_flow_framework - docker-compose up otelcol clickhouse dashboard + docker-compose --profile stats up **Collecting data** diff --git a/makefile b/makefile index b2941b236..c602f65df 100644 --- a/makefile +++ b/makefile @@ -50,7 +50,7 @@ lint: venv .PHONY: lint docker_up: - docker-compose up -d + docker-compose --profile context_storage --profile stats up -d .PHONY: docker_up wait_db: docker_up From b20a14614402de78e1b8f9c336a5022142edc93d Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Tue, 17 Oct 2023 01:57:01 +0300 Subject: [PATCH 12/22] Feat/update guide (#209) * update basic conceptions * Update basic concepts tutorial * fix problems with list formatting * rename file to basic_conceptions; update file text; * update guide paragraphs * Add user guide update * update basic_conceptions * Minor updates to basic concepts guide * update tutor * update basic conceptions: script * update fallback_response * Update script && fallback function * fix section structure * add links to other guides * fix codeblocks --------- Co-authored-by: Roman Zlobin --- docs/source/user_guides.rst | 4 +- docs/source/user_guides/basic_conceptions.rst | 455 +++++++++++------- 2 files changed, 272 insertions(+), 187 deletions(-) diff --git a/docs/source/user_guides.rst b/docs/source/user_guides.rst index fa274db88..024c84ecc 100644 --- a/docs/source/user_guides.rst +++ b/docs/source/user_guides.rst @@ -1,10 +1,10 @@ User guides ----------- -:doc:`Basic conceptions <./user_guides/basic_conceptions>` +:doc:`Basic concepts <./user_guides/basic_conceptions>` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the ``basic conceptions`` tutorial the basics of DFF are described, +In the ``basic concepts`` tutorial the basics of DFF are described, those include but are not limited to: dialog graph creation, specifying start and fallback nodes, setting transitions and conditions, using ``Context`` object in order to receive information about current script execution. diff --git a/docs/source/user_guides/basic_conceptions.rst b/docs/source/user_guides/basic_conceptions.rst index 6cd4d2964..5344143e0 100644 --- a/docs/source/user_guides/basic_conceptions.rst +++ b/docs/source/user_guides/basic_conceptions.rst @@ -4,32 +4,58 @@ Basic Concepts Introduction ~~~~~~~~~~~~ -Dialog Flow Framework helps its users create conversational services, which is done by -defining a specialized dialog graph that dictates the behaviour of the dialog service. -This dialog graph essentially represents the dialog script that guides the conversation -between the chat-bot and the user. +The Dialog Flow Framework (DFF) is a modern tool for designing conversational services. -DFF leverages a specialized language known as a Domain-Specific Language (DSL) -to enable developers to quickly write and comprehend dialog graphs. -This DSL greatly simplifies the process of designing complex conversations and handling -various user inputs, making it easier to build sophisticated conversational systems. +DFF introduces a specialized Domain-Specific Language (DSL) based on standard Python functions and data structures +which makes it very easy for developers with any level of expertise to design a script for user - bot interaction. +The script comes in a form of a *dialog graph* where +each node equals a specific state of the dialog, i.e. a specific conversation turn. +The graph includes the majority of the conversation logic, and covers one or several user scenarios, all in a single Python dict. -DFF installation and requirements -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In this tutorial, we describe the basics of DFF API, +and walk you through the process of creating and maintaining a conversational service with the help of DFF. -For this very basic tutorial we will need only the core dependencies of DFF. -They can be installed via the following command: + +Creating Conversational Services with DFF +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Installation +============ + +To get started with DFF, you need to install its core dependencies, which can be done using the following command: .. code-block:: shell pip3 install dff -Example conversational chat-bot -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Defining Dialogue Goals and User Scenarios +========================================== + +To create a conversational service using Dialog Flow Framework (DFF), you start by defining the overall dialogue goal +and breaking down the dialogue into smaller scenarios based on the user intents or actions that you want to cover. +DFF's Domain-Specific Language makes it easy to break down the dialog script into `flows`, i.e. named groups of nodes +unified by a specific purpose. + +For instance, if one of the dialog options that we provide to the user is to play a game, +the bot can have a 'game' flow that contains dialog states related to this subject, while other flows +cover other topics, e.g. 'time' flow can include questions and answers related to telling the time, +'weather' to telling the weather, etc. + +Creating Dialogue Flows for User Scenarios +========================================== -Let us go through the creation of a simple bot that would play a (virtual) ping-pong game with its users. -It would also greet them and handle exceptions. -First, we define the chat-bot in pseudo language: +Once you have DFF installed, you can define dialog flows targeting various user scenarios +and combine them in a global script object. A flow consists of one or more nodes +that represent conversation turns. + +.. note:: + + In other words, the script object has 3 levels of nestedness: + **script - flow - node** + +Let's assume that the only user scenario of the service is the chat bot playing ping pong with the user. +The practical implementation of this is that the bot is supposed to reply 'pong' to messages that say 'ping' +and handle any other messages as exceptions. The pseudo-code for the said flow would be as follows: .. code-block:: text @@ -40,52 +66,59 @@ First, we define the chat-bot in pseudo language: Respond with "Pong!" Repeat this behaviour - If user writes something else: - Respond with "You should've written 'Ping', not '[USER MESSAGE]'!" - Go to responding with "Hi! Let's play ping-pong!" if user writes anything - If user writes something else: - Respond with "You should've started the dialog with 'Hello!'" + Respond with "That was against the rules" Go to responding with "Hi! Let's play ping-pong!" if user writes anything -Later in this tutorial we will create this chat-bot using DFF, starting from the very basics -and then elaborating on more complicated topics. - -Example chat-bot graph -~~~~~~~~~~~~~~~~~~~~~~ +This leaves us with a single dialog flow in the dialog graph that we lay down below, with the annotations for +each part of the graph available under the code snippet. -Let's start from creating the very simple dialog agent: +Example flow & script +===================== .. code-block:: python + :linenos: from dff.pipeline import Pipeline from dff.script import TRANSITIONS, RESPONSE, Message import dff.script.conditions as cnd ping_pong_script = { - "ping_pong_flow": { + "greeting_flow": { "start_node": { - RESPONSE: Message(), + RESPONSE: Message(), # the response of the initial node is skipped TRANSITIONS: { - "greeting_node": cnd.exact_match(Message(text="Hello!")), + ("greeting_flow", "greeting_node"): + cnd.exact_match(Message(text="/start")), }, }, "greeting_node": { - RESPONSE: Message(text="Hi! Let's play ping-pong!"), + RESPONSE: Message(text="Hi!"), + TRANSITIONS: { + ("ping_pong_flow", "game_start_node"): + cnd.exact_match(Message(text="Hello!")) + } + }, + "fallback_node": { + RESPONSE: fallback_response, TRANSITIONS: { - "response_node": cnd.exact_match(Message(text="Ping!")), + ("greeting_flow", "greeting_node"): cnd.true(), }, }, - "response_node": { - RESPONSE: Message(text="Pong!"), + }, + "ping_pong_flow": { + "game_start_node": { + RESPONSE: Message(text="Let's play ping-pong!"), TRANSITIONS: { - "response_node": cnd.exact_match(Message(text="Ping!")), + ("ping_pong_flow", "response_node"): + cnd.exact_match(Message(text="Ping!")), }, }, - "fallback_node": { - RESPONSE: Message(text="That was against the rules!"), + "response_node": { + RESPONSE: Message(text="Pong!"), TRANSITIONS: { - "greeting_node": cnd.true(), + ("ping_pong_flow", "response_node"): + cnd.exact_match(Message(text="Ping!")), }, }, }, @@ -93,41 +126,38 @@ Let's start from creating the very simple dialog agent: pipeline = Pipeline.from_script( ping_pong_script, - start_label=("ping_pong_flow", "start_node"), - fallback_label=("ping_pong_flow", "fallback_node"), + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), ) if __name__ == "__main__": pipeline.run() -.. warning:: +The code snippet defines a script with a single dialogue flow that emulates a ping-pong game. +Likewise, if additional scenarios need to be covered, additional flow objects can be embedded into the same script object. - Current dialog agent doesn't support different cases and/or marks in "Ping" - messages, it only supports exact "Ping!" message from user. - It also supports only one standard error message for any error. +* ``ping_pong_script``: The dialog **script** mentioned above is a dictionary that has one or more + dialog flows as its values. -That's what the agent consists of: - -* ``ping_pong_script``: in order to create a dialog agent, a dialog **script** is needed; - a script is a dictionary, where the keys are the names of the flows (that are "sub-dialogs", - used to separate the whole dialog into multiple sub-dialogs). - -* ``ping_pong_flow`` is our behaviour flow; a flow is a separated dialog, containing linked +* ``ping_pong_flow`` is the game emulation flow; it contains linked conversation nodes and possibly some extra data, transitions, etc. -* ``start_node`` is the initial node, contains no response, only transfers user to an other node - according to the first message user sends. - It transfers user to ``greeting_node`` if user writes text message exactly equal to "Hello!". +* A node object is an atomic part of the script. + The required fields of a node object are ``RESPONSE`` and ``TRANSITIONS``. -* Each node contains "RESPONSE" and "TRANSITIONS" elements. +* The ``RESPONSE`` field specifies the response that the dialog agent gives to the user in the current turn. -* ``RESPONSE`` value should be a ``Message`` object, that can contain text, images, - audios, attachments, etc. +* The ``TRANSITIONS`` field specifies the edges of the dialog graph that link the dialog states. + This is a dictionary that maps labels of other nodes to conditions, i.e. callback functions that + return `True` or `False`. These conditions determine whether respective nodes can be visited + in the next turn. + In the example script, we use standard transitions: ``exact_match`` requires the user request to + fully match the provided text, while ``true`` always allows a transition. However, passing custom + callbacks that implement arbitrary logic is also an option. -* ``TRANSITIONS`` value should be a dict, containing node names and conditions, - that should be met in order to go to the node specified. - Here, we can see two different types of transitions: ``exact_match`` requires user message text to - match the provided text exactly, while ``true`` allowes unconditional transition. +* ``start_node`` is the initial node, which contains an empty response and only transfers user to another node + according to the first message user sends. + It transfers user to ``greeting_node`` if user writes text message exactly equal to "Hello!". * ``greeting_node`` is the node that will greet user and propose him a ping-pong game. It transfers user to ``response_node`` if user writes text message exactly equal to "Ping!". @@ -135,169 +165,224 @@ That's what the agent consists of: * ``response_node`` is the node that will play ping-pong game with the user. It transfers user to ``response_node`` if user writes text message exactly equal to "Ping!". -* ``fallback_node`` is an "exception handling node"; user will be transferred here if in any node - no transition for the message given by user is found. +* ``fallback_node`` is an "exception handling node"; user will be transferred here if + none of the transition conditions (see ``TRANSITIONS``) is satisfied. It transfers user to ``greeting_node`` no matter what user writes. -* ``pipeline`` is a special object that processes user requests according to provided script. - In order to create a pipeline, the script should be provided and two two-string tuples: - the first specifies initial node flow and name and the second (optional) specifies fallback - node flow and name (if not provided it equals to the first one by default). +* ``pipeline`` is a special object that traverses the script graph based on the values of user input. + It is also capable of executing custom actions that you want to run on every turn of the conversation. + The pipeline can be initialized with a script, and with labels of two nodes: + the entrypoint of the graph, aka the 'start node', and the 'fallback node' + (if not provided it defaults to the same node as 'start node'). .. note:: - See `tutorial on basic dialog structure`_. + See `tutorial on basic dialog structure <../tutorials/tutorials.script.core.1_basics.html>`_. + +Processing Definition +===================== + +.. note:: + + The topic of this section is explained in greater detail in the following tutorials: + + * `Pre-response processing <../tutorials/tutorials.script.core.7_pre_response_processing.html>`_ + * `Pre-transitions processing <../tutorials/tutorials.script.core.9_pre_transitions_processing.html>`_ + * `Pipeline processors <../tutorials/tutorials.pipeline.2_pre_and_post_processors.html>`_ + +Processing user requests and extracting additional parameters is a crucial part of building a conversational bot. +DFF allows you to define how user requests will be processed to extract additional parameters. +This is done by passing callbacks to a special ``PROCESSING`` fields in a Node dict. -Advanced graph features -~~~~~~~~~~~~~~~~~~~~~~~ +* User input can be altered with ``PRE_RESPONSE_PROCESSING`` and will happen **before** response generation. See `tutorial on pre-response processing`_. +* Node response can be modified with ``PRE_TRANSITIONS_PROCESSING`` and will happen **after** response generation but **before** transition to the next node. See `tutorial on pre-transition processing`_. -Right now the agent we have created is a very simple one and does not behave **exactly** as we wanted -our bot to behave. Let's see how we can improve our script: +Depending on the requirements of your bot and the dialog goal, you may need to interact with external databases or APIs to retrieve data. +For instance, if a user wants to know a schedule, you may need to access a database and extract parameters such as date and location. .. code-block:: python - from dff.pipeline import Pipeline - from dff.script import TRANSITIONS, RESPONSE, Context, Message - import dff.script.conditions as cnd - import dff.script.labels as lbl + import requests + ... + def use_api_processing(ctx: Context, _: Pipeline, *args, **kwargs) -> Context: + # save to the context field for custom info + ctx.misc["api_call_results"] = requests.get("http://schedule.api/day1").json() + return ctx + ... + node = { + RESPONSE: ... + TRANSITIONS: ... + PRE_TRANSITIONS_PROCESSING: {"use_api": use_api_processing} + } - def get_previous_node_name(ctx: Context) -> str: - """ - Get the name of the previous visited script node. - """ - last_label = sorted(list(ctx.labels))[-2] if len(ctx.labels) >= 2 else None - # labels store the list of nodes the bot transitioned to, - # so the second to last label would be the label of a previous node - return ctx.labels[last_label][1] if last_label is not None else "start_node" - # label is a two-item tuple used to identify a node, - # the first element is flow name and the second is node name +.. note:: + + This function uses ``Context`` to store the result of a request for other functions to use. + Context is a data structure that keeps all the information about a specific conversation. + + To learn more about ``Context`` see the `relevant guide <../user_guides/context_guide.html>`__. + +If you retrieve data from the database or API, it's important to validate it to ensure it meets expectations. + +Since DFF extensively leverages pydantic, you can resort to the validation tools of this feature-rich library. +For instance, given that each processing routine is a callback, you can use tools like pydantic's `validate_call` +to ensure that the returned values match the function signature. +Error handling logic can also be incorporated into these callbacks. + +Generating a bot Response +========================= + +Generating a bot response involves creating a text or multimedia response that will be delivered to the user. +Response is defined in the ``RESPONSE`` section of each node and should be either a ``Message`` object, +that can contain text, images, audios, attachments, etc., or a callback that returns a ``Message``. +The latter allows you to customize the response based on the specific scenario and user input. + +.. code-block:: python + + def sample_response(ctx: Context, _: Pipeline, *args, **kwargs) -> Message: + if ctx.misc["user"] == 'vegan': + return Message(text="Here is a list of vegan cafes.") + return Message(text="Here is a list of cafes.") + +Handling Fallbacks +================== + +In DFF, you should provide handling for situations where the user makes requests +that do not trigger any of the transitions specified in the script graph. +To cover that use case, DFF requires you to define a fallback node that the agent will move to +when no adequate transition has been found. + +Like other nodes, the fallback node can either use a message or a callback to produce a response +which gives you a lot of freedom in creating situationally appropriate error messages. +Create friendly error messages and, if possible, suggest alternative options. +This ensures a smoother user experience even when the bot encounters unexpected inputs. + +.. code-block:: python def fallback_response(ctx: Context, _: Pipeline, *args, **kwargs) -> Message: """ - Generate response for fallback node, according to the previous node - we have been to. - If the previous node was `start_node`, a sample message will be returned, - otherwise the message will include user input. + Generate a special fallback response depending on the situation. """ - if get_previous_node_name(ctx) == "start_node": - return Message(text="You should've started the dialog with 'Hello!'") - elif ctx.last_request is not None: - last_request = ctx.last_request.text - note = f"You should've written 'Ping', not '{last_request}'!" - return Message(text=f"That was against the rules! {note}") + if ctx.last_request is not None: + if ctx.last_request.text != "/start" and ctx.last_label is None: + # an empty last_label indicates start_node + return Message(text="You should've started the dialog with '/start'") + else: + return Message( + text=f"That was against the rules!\n" + f"You should've written 'Ping', not '{ctx.last_request.text}'!" + ) else: raise RuntimeError("Error occurred: last request is None!") - - ping_pong_script = { - "ping_pong_flow": { - "start_node": { - RESPONSE: Message(), - TRANSITIONS: { - lbl.forward(): cnd.exact_match(Message(text="Hello!")), - }, - }, - "greeting_node": { - RESPONSE: Message(text="Hi! Let's play ping-pong!"), - TRANSITIONS: { - lbl.forward(): cnd.regexp(r"^[P|p]ing!?$"), - }, - }, - "ping_pong_node": { - RESPONSE: Message(text="Pong!"), - TRANSITIONS: { - lbl.repeat(): cnd.regexp(r"^[P|p]ing!?$"), - }, - }, - "fallback_node": { - RESPONSE: fallback_response, - TRANSITIONS: { - "greeting_node": cnd.true(), - }, - }, - }, - } +Testing and Debugging +~~~~~~~~~~~~~~~~~~~~~ - pipeline = Pipeline.from_script( - ping_pong_script, - start_label=("ping_pong_flow", "start_node"), - fallback_label=("ping_pong_flow", "fallback_node"), - ) - - if __name__ == "__main__": - pipeline.run() +Periodically testing the conversational service is crucial to ensure it works correctly. +You should also be prepared to debug the code and dialogue logic if problems are discovered during testing. +Thorough testing helps identify and resolve any potential problems in the conversation flow. -That's what we've changed: +The basic testing procedure offered by DFF is end-to-end testing of the pipeline and the script +which ensures that the pipeline yields correct responses for any given input. +It requires a sequence of user request - bot response pairs that form the happy path of your +conversational service. -* ``fallback_node`` has a callback response, it prints different messages depending on the - previous node. +.. code-block:: python -.. note:: + happy_path = ( + (Message(text="/start"), Message(text="Hi!")), + (Message(text="Hello!"), Message(text="Let's play ping-pong!")), + (Message(text="Ping!"), Message(text="Pong!")) + ) - See `tutorial on response functions`_. +A special function is then used to ascertain complete identity of the messages taken from +the happy path and the pipeline. The function will play out a dialog with the pipeline acting as a user while checking returned messages. -* A special function ``get_previous_node_name`` was written to determine the name of the previous - visited node. It utilizes ``labels`` attribute of the ``Context`` object. +.. code-block:: python -.. note:: + from dff.utils.testing.common import check_happy_path - See `guide on Context objects`_. + check_happy_path(pipeline, happy_path) -* Transitions were changed: transitions to next, previous and current node were replaced with special - standard transitions. +Monitoring and Analytics +~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: +Setting up bot performance monitoring and usage analytics is essential to monitor its operation and identify potential issues. +Monitoring helps you understand how users are interacting with the bot and whether any improvements are needed. +Analytics data can provide valuable insights for refining the bot's behavior and responses. - See `tutorial on transitions`_. - -* Conditions were changed: now regular expressions are used to check user text input value. +DFF provides a `statistics` module as an out-of-the-box solution for collecting arbitrary statistical metrics +from your service. Setting up the data collection is as easy as instantiating the relevant class in the same +context with the pipeline. +What's more, the data you obtain can be visualized right away using Apache Superset as a charting engine. .. note:: - See `tutorial on conditions`_. + More information is available in the respective `guide <../user_guides/superset_guide.html>`__. -Further exploration -~~~~~~~~~~~~~~~~~~~ +Iterative Improvement +~~~~~~~~~~~~~~~~~~~~~ -There are still a lot of capabilities of Dialog Flow Framework that remain uncovered by this tutorial. +To continually enhance your chat-bot's performance, monitor user feedback and analyze data on bot usage. +For instance, the statistics or the charts may reveal that some flow is visited by users more frequently or +less frequently than planned. This would mean that adjustments to the transition structure +of the graph need to be made. -For example: +Gradually improve the transition logic and response content based on the data received. +This iterative approach ensures that the bot becomes more effective over time. -* You can use ``GLOBAL`` transitions that will be available from every node in your script. - See `tutorial on global transitions`_. +Data Protection +~~~~~~~~~~~~~~~ -* You can serialize context (available on every transition and response) - to json or dictionary in order to debug it or extract some values. - See `guide on context serialization`_. +Data protection is a critical consideration in bot development, especially when handling sensitive information. -* You can alter user input and modify generated responses. - User input can be altered with ``PRE_RESPONSE_PROCESSING`` and will happen **before** response generation. - See `tutorial on pre-response processing`_. - Node response can be modified with ``PRE_TRANSITION_PROCESSING`` and will happen **after** response generation. - See `tutorial on pre-transition processing`_. +.. note:: -* Additional data ``MISC`` can be added to every node, flow and script itself. - See `tutorial on script MISC`_. + The DFF framework helps ensure the safety of your application by storing the history and other user data present + in the ``Context`` object under unique ids and abstracting the storage logic away from the user interface. + As a result, it offers the basic level of data protection making it impossible to gain unlawful access to personal information. -Conclusion -~~~~~~~~~~ +Documentation +~~~~~~~~~~~~~ -In this tutorial, we explored the basics of Dialog Flow Framework (DFF) to build dynamic conversational services. -By using DFF's intuitive Domain-Specific Language (DSL) and well-structured dialog graphs, we created a simple interaction between user and chat-bot. -We covered installation, understanding the DSL and building dialog graph. -However, this is just the beginning. DFF offers a world of possibilities in conversational chat-bot. -With practice and exploration of advanced features, you can create human-like conversations and reach a wider audience by integrating with various platforms. -Now, go forth, unleash your creativity, and create captivating conversational services with DFF. -Happy building! +Creating documentation is essential for teamwork and future bot maintenance. +Document how different parts of the script work and how the bot covers the expected interaction scenarios. +It is especially important to document the purpose and functionality of callback functions and pipeline services +that you may have in your project, using Python docstrings. +.. code-block:: python -.. _tutorial on basic dialog structure: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.1_basics.html -.. _tutorial on response functions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.3_responses.html -.. _guide on Context objects: ../user_guides/context_guide.html -.. _tutorial on transitions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.4_transitions.html -.. _tutorial on conditions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.2_conditions.html -.. _tutorial on global transitions: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.5_global_transitions.html -.. _guide on context serialization: ../user_guides/context_guide.html#serialization -.. _tutorial on pre-response processing: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.7_pre_response_processing.html -.. _tutorial on pre-transition processing: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.9_pre_transitions_processing.html -.. _tutorial on script MISC: https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.script.core.8_misc.html + def fav_kitchen_response(ctx: Context, _: Pipeline, *args, **kwargs) -> Message: + """ + This function returns a user-targeted response depending on the value + of the 'kitchen preference' slot. + """ + ... + +This documentation serves as a reference for developers involved in the project. + +Scaling +~~~~~~~ + +If your bot becomes popular and requires scaling, consider scalability during development. +Scalability ensures that the bot can handle a growing user base without performance issues. +While having only one application instance will suffice in most cases, there are many ways +how you can adapt the application to a high load environment. + +* With the database connection support that DFF offers out of the box, DFF projects can be easily scaled through sharing the same database between multiple application instances. However, using an external database is required due to the fact that this is the only kind of storage that can be efficiently shared between processes. +* Likewise, using multiple database instances to ensure the availability of data is also an option. +* The structure of the `Context` object makes it easy to vertically partition the data storing different subsets of data across multiple database instances. + +Further reading +~~~~~~~~~~~~~~~ + +* `Tutorial on basic dialog structure <../tutorials/tutorials.script.core.1_basics.html>`_ +* `Tutorial on transitions <../tutorials/tutorials.script.core.4_transitions.html>`_ +* `Tutorial on conditions <../tutorials/tutorials.script.core.2_conditions.html>`_ +* `Tutorial on response functions <../tutorials/tutorials.script.core.3_responses.html>`_ +* `Tutorial on pre-response processing <../tutorials/tutorials.script.core.7_pre_response_processing.html>`_ +* `Tutorial on pre-transition processing <../tutorials/tutorials.script.core.9_pre_transitions_processing.html>`_ +* `Guide on Context <../user_guides/context_guide.html>`_ +* `Tutorial on global transitions <../tutorials/tutorials.script.core.5_global_transitions.html>`_ +* `Tutorial on context serialization <../tutorials/tutorials.script.core.6_context_serialization.html>`_ +* `Tutorial on script MISC <../tutorials/tutorials.script.core.8_misc.html>`_ \ No newline at end of file From 8afc4a3b24c0ed3d3d9935b54b98692deaf957bc Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 17 Oct 2023 02:07:36 +0300 Subject: [PATCH 13/22] update version to 0.6.1 --- dff/__init__.py | 2 +- docs/source/conf.py | 2 +- makefile | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dff/__init__.py b/dff/__init__.py index 1e39010f4..a72f1a88f 100644 --- a/dff/__init__.py +++ b/dff/__init__.py @@ -4,7 +4,7 @@ __author__ = "Denis Kuznetsov" __email__ = "kuznetsov.den.p@gmail.com" -__version__ = "0.6.0" +__version__ = "0.6.1" import nest_asyncio diff --git a/docs/source/conf.py b/docs/source/conf.py index ff1c47525..aec507d90 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,7 +17,7 @@ author = "DeepPavlov" # The full version, including alpha/beta/rc tags -release = "0.6.0" +release = "0.6.1" # -- General configuration --------------------------------------------------- diff --git a/makefile b/makefile index c602f65df..e182bfcdf 100644 --- a/makefile +++ b/makefile @@ -5,7 +5,7 @@ SHELL = /bin/bash PYTHON = python3 VENV_PATH = venv VERSIONING_FILES = setup.py makefile docs/source/conf.py dff/__init__.py -CURRENT_VERSION = 0.6.0 +CURRENT_VERSION = 0.6.1 TEST_COVERAGE_THRESHOLD=95 TEST_ALLOW_SKIP=all # for more info, see tests/conftest.py diff --git a/setup.py b/setup.py index 2ba6651a6..54c234558 100644 --- a/setup.py +++ b/setup.py @@ -216,7 +216,7 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: setup( name="dff", - version="0.6.0", + version="0.6.1", description=description, long_description=long_description, long_description_content_type="text/markdown", From 90dc0c37b1e95d25eeedd1b7fb9fbf5bb1e3e963 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 17 Oct 2023 12:14:23 +0300 Subject: [PATCH 14/22] remove master PR triggers PRs into master come from dev which already has `on: push`. --- .github/workflows/build_and_publish_docs.yml | 1 - .github/workflows/codestyle.yml | 1 - .github/workflows/test_coverage.yml | 1 - .github/workflows/test_full.yml | 1 - 4 files changed, 4 deletions(-) diff --git a/.github/workflows/build_and_publish_docs.yml b/.github/workflows/build_and_publish_docs.yml index 15bbd6cc3..8a2cbdd56 100644 --- a/.github/workflows/build_and_publish_docs.yml +++ b/.github/workflows/build_and_publish_docs.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - dev - - master workflow_dispatch: concurrency: diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index a5592dd88..d86cd4dae 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - dev - - master workflow_dispatch: concurrency: diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml index 8931262b5..e4b0270b4 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/test_coverage.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - dev - - master workflow_dispatch: concurrency: diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index 17005436d..60eccdf47 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - dev - - master workflow_dispatch: concurrency: From 01069a39d8a890142333794add9130a330637bb8 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 17 Oct 2023 21:56:46 +0300 Subject: [PATCH 15/22] fix api link --- docs/source/user_guides/context_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/user_guides/context_guide.rst b/docs/source/user_guides/context_guide.rst index 1dc47712c..464d963a7 100644 --- a/docs/source/user_guides/context_guide.rst +++ b/docs/source/user_guides/context_guide.rst @@ -86,7 +86,7 @@ API This sections describes the API of the ``Context`` class. For more information, such as method signatures, see -`API reference <./apiref/dff.script.core.context.html#dff.script.core.context.Context>`__. +`API reference <../apiref/dff.script.core.context.html#dff.script.core.context.Context>`__. Attributes ========== From 5fda101333df1e0345038c618fbbcce358085157 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Thu, 26 Oct 2023 17:40:20 +0300 Subject: [PATCH 16/22] Update PR template & remove PR body check (#258) Checklist now consists only of self-review. Instead a to-consider list is added without checkboxes. Also, the bot that checks the body of PRs is removed. --- .github/PULL_REQUEST_TEMPLATE.md | 17 +++++--- .github/process_github_events.py | 64 ----------------------------- .github/workflows/event_handler.yml | 28 ------------- 3 files changed, 12 insertions(+), 97 deletions(-) delete mode 100644 .github/process_github_events.py delete mode 100644 .github/workflows/event_handler.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 95c528e45..e9a8189c6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,17 @@ # Description -*Please describe here what changes are made and why. If this pull request fixes a GitHub issue, reference it.* +*Please describe here what changes are made and why.* # Checklist -- [ ] I have covered the code with tests -- [ ] I have added comments to my code to help others understand it -- [ ] I have updated the documentation to reflect the changes -- [ ] I have performed a self-review of the changes \ No newline at end of file +- [ ] I have performed a self-review of the changes + +*List here tasks to do in order to complete this PR.* + +# To Consider + +- Add tests (if functionality is changed) +- Update API reference / tutorials / guides +- Update CONTRIBUTING.md (if devel workflow is changed) +- Update `.ignore` files, scripts (such as `lint`), distribution manifest (if files are added/deleted) +- Search for references to changed entities in the codebase \ No newline at end of file diff --git a/.github/process_github_events.py b/.github/process_github_events.py deleted file mode 100644 index 2a15743b4..000000000 --- a/.github/process_github_events.py +++ /dev/null @@ -1,64 +0,0 @@ -# flake8: noqa -import os -import json -import re -import requests - -GITHUB_ARGS = { - "GITHUB_TOKEN": None, - "GITHUB_REPOSITORY": None, - "GITHUB_EVENT_PATH": None, - "GITHUB_EVENT_NAME": None, -} - -for arg in GITHUB_ARGS: - GITHUB_ARGS[arg] = os.getenv(arg) - - if GITHUB_ARGS[arg] is None: - raise RuntimeError(f"`{arg}` is not set") - else: - print(arg, GITHUB_ARGS[arg]) - - -def on_opened_pull_request(event_info: dict): - print("Opened pr") - with open(".github/PULL_REQUEST_TEMPLATE.md", "r", encoding="utf-8") as fd: - contents = fd.read() - template_body = re.escape(contents).replace("\-\ \[\ \]", "\-\ \[[ x]\]").replace("\\\n", "(\n|\r|\r\n)") - template_body_pattern = re.compile(template_body) - # this matches any string that equals PULL_REQUEST_TEMPLATE with checklist items checked or unchecked - # (i.e. if instead of `- [ ]` string has `- [x]` it also counts) - - pr_number = event_info["pull_request"]["number"] - pr_body = event_info["pull_request"]["body"] - - if pr_body is None or pr_body == "" or template_body_pattern.match(pr_body) is not None: - print("Match found") - headers = { - "Accept": "application/vnd.github+json", - "Authorization": f'Bearer {GITHUB_ARGS["GITHUB_TOKEN"]}', - "X-GitHub-Api-Version": "2022-11-28", - } - - data = '{"body": "Please provide a description: what is changed and why.","event": "COMMENT","comments": []}' - - response = requests.post( - f'https://api.github.com/repos/{GITHUB_ARGS["GITHUB_REPOSITORY"]}/pulls/{pr_number}/reviews', - headers=headers, - data=data, - ) - - if not response.status_code == 200: - raise RuntimeError(response.__dict__) - - -def main(): - with open(GITHUB_ARGS["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as fd: - event_info = json.load(fd) - print(f"event info: {event_info}") - if GITHUB_ARGS["GITHUB_EVENT_NAME"] == "pull_request_target" and event_info["action"] == "opened": - on_opened_pull_request(event_info) - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/event_handler.yml b/.github/workflows/event_handler.yml deleted file mode 100644 index 4c0832171..000000000 --- a/.github/workflows/event_handler.yml +++ /dev/null @@ -1,28 +0,0 @@ -on: - pull_request_target: - types: [ opened ] - -jobs: - event_handler: - strategy: - fail-fast: false - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: set up python 3.10 - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: install dependencies - run: python -m pip install requests - shell: bash - - - name: handle event - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: python .github/process_github_events.py - shell: bash \ No newline at end of file From 9b2d45fedc4377bb8af70e1fe1575c77cf3e2c39 Mon Sep 17 00:00:00 2001 From: ruthenian8 Date: Fri, 27 Oct 2023 10:15:16 +0300 Subject: [PATCH 17/22] Fix/dashboard (#227) * update dashboard workflow * run on workflow_dispatch * trigger on PR to dev for debugging * update build push action * update context && file * remove trigger on PR * add sample data provider * Update dashboard, finalize data provider * Add extractors for requests and responses as default instrumentors * add 'get_last_request', 'get_last_response'; add charts for requests and responses; update test * Update sections in superset_guide; update extractor functions tutor * doc build fix: Change functions to func names in __all__ * Apply suggestions by RLKRO from code review Co-authored-by: Roman Zlobin * intermediary update; update data provider tutor; update charts; * Apply suggestions from code review by RLKRO Co-authored-by: Roman Zlobin * Upload annotations.png via GUI * Update requests & responses charts; update superset_guide * expand the Superset guide with chart creation instructions;Add links to Superset documentation * update graph plot; update dataset to get flow lag; update data provider * update guide; move sample data collector to /utils * update links to avoid sphinx warnings * update dashboard description; update tutorials * update superset guide * update label extractor; add infile parameter to main; remove link to sample_data_provider * update tests * update format * minor doc improvements * update dashboard datasource config * add dff_stats dataset; link charts to dff_stats dataset * update tests and documentation to reflect config changes * update formatting * add the test configuration file * move get_current_label to after_handler in sample_data_provider.py * update sql with if statements * specify healthcheck for clickhouse and wait for healthcheck with otlp; make a variable for superset metadata db; adjust dockerfile to use configuration overrides * add dashboard-metadata docker image; adjust to run on port 5433 * Update .github/workflows/update_dashboard.yml Apply suggestions from @RLKRo Co-authored-by: Roman Zlobin * Update dff/stats/cli.py Apply suggestions to label_lag/flow_lag Co-authored-by: Roman Zlobin * remove query statements from dashboard files; remove filter values from charts; update docker-compose healthcheck; apply formatting * wait for pg database; sleep to avoid process conflict * Apply suggestions by @RLKRo Apply suggestions by @RLKRo Co-authored-by: Roman Zlobin * Update config files; Update sql expressions in CLI; update healthcheck expression; create volume for dashboard metadata * convert request_id to int in final_nodes * remove sql in final_nodes * update history filter to numerical range type & change datasource to node_stats * remove utils include from manifest --------- Co-authored-by: Roman Zlobin --- .env_file | 3 +- .github/workflows/update_dashboard.yml | 4 +- .../Current_topic_slot_bar_chart_4.yaml | 72 + ...Current_topic_time_series_bar_chart_2.yaml | 86 ++ .../charts/Flow_visit_ratio_monitor_1.yaml | 78 - .../charts/Flow_visit_ratio_monitor_13.yaml | 101 ++ .../charts/Node_Visits_2.yaml | 59 - .../charts/Node_Visits_7.yaml | 89 ++ .../charts/Node_counts_3.yaml | 64 +- .../charts/Node_visit_ratio_monitor_4.yaml | 86 -- .../charts/Node_visit_ratio_monitor_8.yaml | 109 ++ .../charts/Node_visits_cloud_5.yaml | 48 - .../charts/Node_visits_ratio_6.yaml | 68 +- .../charts/Node_visits_sunburst_5.yaml | 76 + .../charts/Node_visits_sunburst_7.yaml | 69 - .../charts/Rating_slot_line_chart_1.yaml | 92 ++ .../charts/Requests_17.yaml | 59 + .../charts/Responses_16.yaml | 59 + .../Service_load_max_dialogue_length_8.yaml | 83 -- .../superset_dashboard/charts/Table_10.yaml | 34 - .../superset_dashboard/charts/Table_14.yaml | 38 + ...labels_11.yaml => Terminal_labels_15.yaml} | 0 .../charts/Transition_counts_12.yaml | 82 +- .../charts/Transition_layout_10.yaml | 101 ++ .../charts/Transition_layout_13.yaml | 62 - .../charts/Transition_ratio_chord_11.yaml | 86 ++ .../charts/Transition_ratio_chord_14.yaml | 48 - .../dashboards/DFF_Stats_1.yaml | 555 ------- .../DFF_statistics_dashboard_1.yaml | 1291 +++++++++++++++++ .../dff_database/dff_final_nodes.yaml | 13 +- .../datasets/dff_database/dff_node_stats.yaml | 26 +- .../datasets/dff_database/dff_stats.yaml | 146 ++ dff/config/superset_dashboard/metadata.yaml | 2 +- dff/stats/__main__.py | 16 +- dff/stats/cli.py | 69 +- dff/stats/default_extractors.py | 41 +- dff/stats/instrumentor.py | 12 +- dff/utils/docker/dockerfile_stats | 2 + dff/utils/docker/entrypoint_stats.sh | 1 + dff/utils/docker/superset_config_docker.py | 32 + dff/utils/testing/toy_script.py | 140 ++ docker-compose.yml | 33 + docs/source/_static/images/annotations.png | Bin 0 -> 64834 bytes docs/source/user_guides/superset_guide.rst | 103 +- makefile | 2 +- tests/stats/test_defaults.py | 14 +- tests/stats/test_main.py | 20 +- tutorials/stats/1_extractor_functions.py | 17 +- tutorials/stats/2_pipeline_integration.py | 10 +- utils/stats/sample_data_provider.py | 127 ++ 50 files changed, 3142 insertions(+), 1286 deletions(-) create mode 100644 dff/config/superset_dashboard/charts/Current_topic_slot_bar_chart_4.yaml create mode 100644 dff/config/superset_dashboard/charts/Current_topic_time_series_bar_chart_2.yaml delete mode 100644 dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_1.yaml create mode 100644 dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml delete mode 100644 dff/config/superset_dashboard/charts/Node_Visits_2.yaml create mode 100644 dff/config/superset_dashboard/charts/Node_Visits_7.yaml delete mode 100644 dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_4.yaml create mode 100644 dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml delete mode 100644 dff/config/superset_dashboard/charts/Node_visits_cloud_5.yaml create mode 100644 dff/config/superset_dashboard/charts/Node_visits_sunburst_5.yaml delete mode 100644 dff/config/superset_dashboard/charts/Node_visits_sunburst_7.yaml create mode 100644 dff/config/superset_dashboard/charts/Rating_slot_line_chart_1.yaml create mode 100644 dff/config/superset_dashboard/charts/Requests_17.yaml create mode 100644 dff/config/superset_dashboard/charts/Responses_16.yaml delete mode 100644 dff/config/superset_dashboard/charts/Service_load_max_dialogue_length_8.yaml delete mode 100644 dff/config/superset_dashboard/charts/Table_10.yaml create mode 100644 dff/config/superset_dashboard/charts/Table_14.yaml rename dff/config/superset_dashboard/charts/{Terminal_labels_11.yaml => Terminal_labels_15.yaml} (100%) create mode 100644 dff/config/superset_dashboard/charts/Transition_layout_10.yaml delete mode 100644 dff/config/superset_dashboard/charts/Transition_layout_13.yaml create mode 100644 dff/config/superset_dashboard/charts/Transition_ratio_chord_11.yaml delete mode 100644 dff/config/superset_dashboard/charts/Transition_ratio_chord_14.yaml delete mode 100644 dff/config/superset_dashboard/dashboards/DFF_Stats_1.yaml create mode 100644 dff/config/superset_dashboard/dashboards/DFF_statistics_dashboard_1.yaml create mode 100644 dff/config/superset_dashboard/datasets/dff_database/dff_stats.yaml create mode 100644 dff/utils/docker/superset_config_docker.py create mode 100644 docs/source/_static/images/annotations.png create mode 100644 utils/stats/sample_data_provider.py diff --git a/.env_file b/.env_file index 15b7a209a..6295e4946 100644 --- a/.env_file +++ b/.env_file @@ -19,4 +19,5 @@ CLICKHOUSE_USER=username CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 CLICKHOUSE_PASSWORD=pass SUPERSET_USERNAME=superset -SUPERSET_PASSWORD=superset \ No newline at end of file +SUPERSET_PASSWORD=superset +SUPERSET_METADATA_PORT=5433 \ No newline at end of file diff --git a/.github/workflows/update_dashboard.yml b/.github/workflows/update_dashboard.yml index c06e145aa..72042885f 100644 --- a/.github/workflows/update_dashboard.yml +++ b/.github/workflows/update_dashboard.yml @@ -5,8 +5,8 @@ on: branches: - 'master' paths: - - 'dff/utils/docker/dockerfile_stats' - - 'dff/utils/docker/entrypoint_stats.sh' + - 'dff/utils/docker/**' + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/dff/config/superset_dashboard/charts/Current_topic_slot_bar_chart_4.yaml b/dff/config/superset_dashboard/charts/Current_topic_slot_bar_chart_4.yaml new file mode 100644 index 000000000..57deb8a71 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Current_topic_slot_bar_chart_4.yaml @@ -0,0 +1,72 @@ +slice_name: Current topic slot [bar chart] +description: null +certified_by: null +certification_details: null +viz_type: dist_bar +params: + datasource: 3__table + viz_type: dist_bar + slice_id: 6 + granularity_sqla: start_time + time_range: No filter + metrics: + - count + adhoc_filters: + - clause: WHERE + comparator: get_slots + datasourceWarning: false + expressionType: SIMPLE + filterOptionName: filter_sz14lhn7d1d_c0zqn5dpgk + isExtra: false + isNew: false + operator: == + operatorId: EQUALS + sqlExpression: null + subject: data_key + - clause: WHERE + comparator: null + datasourceWarning: false + expressionType: SQL + filterOptionName: filter_945dhn41x2m_sci7gkxy7o + isExtra: false + isNew: false + operator: null + sqlExpression: JSON_VALUE(data, '$.current_topic') <> '' + subject: null + groupby: + - request_id + columns: + - expressionType: SQL + label: My column + sqlExpression: JSON_VALUE(data, '$.current_topic') + datasourceWarning: false + row_limit: 10000 + order_desc: true + color_scheme: echarts4Colors + show_legend: true + rich_tooltip: true + bar_stacked: true + order_bars: false + y_axis_format: SMART_NUMBER + y_axis_label: Topic counts + y_axis_bounds: + - null + - null + x_axis_label: Dialog turn + bottom_margin: auto + x_ticks_layout: auto + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_slots"}],"extras":{"having":"","where":"(JSON_VALUE(data, + ''$.current_topic'') <> '''')"},"applied_time_extras":{},"columns":["request_id",{"expressionType":"SQL","label":"My + column","sqlExpression":"JSON_VALUE(data, ''$.current_topic'')","datasourceWarning":false}],"metrics":["count"],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"3__table","viz_type":"dist_bar","slice_id":6,"granularity_sqla":"start_time","time_range":"No + filter","metrics":["count"],"adhoc_filters":[{"clause":"WHERE","comparator":"get_slots","datasourceWarning":false,"expressionType":"SIMPLE","filterOptionName":"filter_sz14lhn7d1d_c0zqn5dpgk","isExtra":false,"isNew":false,"operator":"==","operatorId":"EQUALS","sqlExpression":null,"subject":"data_key"},{"clause":"WHERE","comparator":null,"datasourceWarning":false,"expressionType":"SQL","filterOptionName":"filter_945dhn41x2m_sci7gkxy7o","isExtra":false,"isNew":false,"operator":null,"sqlExpression":"JSON_VALUE(data, + ''$.current_topic'') <> ''''","subject":null}],"groupby":["request_id"],"columns":[{"expressionType":"SQL","label":"My + column","sqlExpression":"JSON_VALUE(data, ''$.current_topic'')","datasourceWarning":false}],"row_limit":10000,"order_desc":true,"color_scheme":"echarts4Colors","show_legend":true,"rich_tooltip":true,"bar_stacked":true,"order_bars":false,"y_axis_format":"SMART_NUMBER","y_axis_label":"Topic + counts","y_axis_bounds":[null,null],"x_axis_label":"Dialog turn","bottom_margin":"auto","x_ticks_layout":"auto","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: a70c05d0-770b-4068-a55d-934283f5b1bb +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Current_topic_time_series_bar_chart_2.yaml b/dff/config/superset_dashboard/charts/Current_topic_time_series_bar_chart_2.yaml new file mode 100644 index 000000000..26ad821d3 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Current_topic_time_series_bar_chart_2.yaml @@ -0,0 +1,86 @@ +slice_name: Current topic [time series bar chart] +description: null +certified_by: null +certification_details: null +viz_type: echarts_timeseries_bar +params: + datasource: 3__table + viz_type: echarts_timeseries_bar + slice_id: 10 + granularity_sqla: start_time + time_grain_sqla: PT1M + time_range: No filter + metrics: + - count + groupby: + - datasourceWarning: false + expressionType: SQL + label: context_id + sqlExpression: JSON_VALUE(data, '$.current_topic') + adhoc_filters: + - clause: WHERE + comparator: get_slots + datasourceWarning: false + expressionType: SIMPLE + filterOptionName: filter_8tft5fr07ea_urtdezftgn + isExtra: false + isNew: false + operator: == + operatorId: EQUALS + sqlExpression: null + subject: data_key + - clause: WHERE + comparator: null + datasourceWarning: false + expressionType: SQL + filterOptionName: filter_4ffdpny1zzm_vmlo11yw7i + isExtra: false + isNew: false + operator: null + sqlExpression: JSON_VALUE(data, '$.current_topic') <> '' + subject: null + order_desc: true + row_limit: 10000 + truncate_metric: true + show_empty_columns: true + comparison_type: values + annotation_layers: [] + forecastPeriods: 10 + forecastInterval: 0.8 + orientation: vertical + x_axis_title: Time axis + x_axis_title_margin: 30 + y_axis_title: Value counts over time + y_axis_title_margin: 30 + y_axis_title_position: Left + color_scheme: echarts4Colors + show_value: false + stack: true + only_total: true + show_legend: true + legendType: scroll + legendOrientation: top + x_axis_time_format: smart_date + y_axis_format: SMART_NUMBER + y_axis_bounds: + - null + - null + rich_tooltip: true + tooltipSortByMetric: true + tooltipTimeFormat: smart_date + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_slots"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(JSON_VALUE(data, + ''$.current_topic'') <> '''')"},"applied_time_extras":{},"columns":[{"datasourceWarning":false,"expressionType":"SQL","label":"context_id","sqlExpression":"JSON_VALUE(data, + ''$.current_topic'')"}],"metrics":["count"],"orderby":[["count",false]],"annotation_layers":[],"row_limit":10000,"series_columns":[{"datasourceWarning":false,"expressionType":"SQL","label":"context_id","sqlExpression":"JSON_VALUE(data, + ''$.current_topic'')"}],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["context_id"],"aggregates":{"count":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"count":null},"level":0,"inplace":true}},{"operation":"flatten"}]}],"form_data":{"datasource":"3__table","viz_type":"echarts_timeseries_bar","slice_id":10,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"No + filter","metrics":["count"],"groupby":[{"datasourceWarning":false,"expressionType":"SQL","label":"context_id","sqlExpression":"JSON_VALUE(data, + ''$.current_topic'')"}],"adhoc_filters":[{"clause":"WHERE","comparator":"get_slots","datasourceWarning":false,"expressionType":"SIMPLE","filterOptionName":"filter_8tft5fr07ea_urtdezftgn","isExtra":false,"isNew":false,"operator":"==","operatorId":"EQUALS","sqlExpression":null,"subject":"data_key"},{"clause":"WHERE","comparator":null,"datasourceWarning":false,"expressionType":"SQL","filterOptionName":"filter_4ffdpny1zzm_vmlo11yw7i","isExtra":false,"isNew":false,"operator":null,"sqlExpression":"JSON_VALUE(data, + ''$.current_topic'') <> ''''","subject":null}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title":"Time + axis","x_axis_title_margin":30,"y_axis_title":"Value counts over time","y_axis_title_margin":30,"y_axis_title_position":"Left","color_scheme":"echarts4Colors","show_value":false,"stack":true,"only_total":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipSortByMetric":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: f8215b4d-cdaf-489a-90b2-040da840ab35 +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_1.yaml b/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_1.yaml deleted file mode 100644 index 4fb4c2124..000000000 --- a/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_1.yaml +++ /dev/null @@ -1,78 +0,0 @@ -slice_name: Flow visit ratio monitor -description: null -certified_by: null -certification_details: null -viz_type: echarts_timeseries_bar -params: - datasource: 2__table - viz_type: echarts_timeseries_bar - slice_id: 1 - granularity_sqla: start_time - time_grain_sqla: null - time_range: No filter - metrics: - - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_waxxrrnkwwm_zudaex5z8bh - sqlExpression: null - groupby: - - flow_label - contributionMode: column - adhoc_filters: [] - order_desc: true - row_limit: 10000 - truncate_metric: true - show_empty_columns: true - comparison_type: values - annotation_layers: [] - forecastPeriods: 10 - forecastInterval: 0.8 - orientation: vertical - x_axis_title_margin: 15 - y_axis_title: '' - y_axis_title_margin: 50 - y_axis_title_position: Left - color_scheme: supersetColors - stack: true - only_total: true - zoomable: true - show_legend: true - legendType: scroll - legendOrientation: top - x_axis_time_format: smart_date - y_axis_format: SMART_NUMBER - y_axis_bounds: - - null - - null - rich_tooltip: true - tooltipTimeFormat: smart_date - extra_form_data: {} - dashboards: - - 1 -query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No - filter","granularity":"start_time","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":["flow_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":1,"granularity_sqla":"start_time","time_grain_sqla":null,"time_range":"No - filter","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"groupby":["flow_label"],"contributionMode":"column","adhoc_filters":[],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title_margin":15,"y_axis_title":"","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"supersetColors","stack":true,"only_total":true,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' -cache_timeout: null -uuid: ba02528b-184b-4304-b027-f2b7d9011ab0 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml b/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml new file mode 100644 index 000000000..dfa036ee4 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml @@ -0,0 +1,101 @@ +slice_name: Flow visit ratio monitor +description: null +certified_by: null +certification_details: null +viz_type: echarts_timeseries_bar +params: + datasource: 2__table + viz_type: echarts_timeseries_bar + slice_id: 13 + granularity_sqla: start_time + time_grain_sqla: PT1M + time_range: No filter + metrics: + - aggregate: COUNT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 1 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT(context_id) + optionName: metric_waxxrrnkwwm_zudaex5z8bh + sqlExpression: null + groupby: + - flow_label + contributionMode: column + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_xqjhabd7z8p_05dlj2lmg6ue + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_822k33tkrei_irkb2zdjds + order_desc: true + row_limit: 10000 + truncate_metric: true + show_empty_columns: true + comparison_type: values + annotation_layers: [] + forecastPeriods: 10 + forecastInterval: 0.8 + orientation: vertical + x_axis_title_margin: 15 + y_axis_title: '' + y_axis_title_margin: 50 + y_axis_title_position: Left + color_scheme: echarts4Colors + stack: true + only_total: true + zoomable: true + show_legend: true + legendType: scroll + legendOrientation: top + x_axis_time_format: smart_date + y_axis_format: SMART_NUMBER + y_axis_bounds: + - null + - null + rich_tooltip: true + tooltipTimeFormat: smart_date + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"DATEADD(DATETIME(\"now\"), + -1, day) : now","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["flow_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":13,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"DATEADD(DATETIME(\"now\"), + -1, day) : now","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"groupby":["flow_label"],"contributionMode":"column","adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_xqjhabd7z8p_05dlj2lmg6ue"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_822k33tkrei_irkb2zdjds"}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title_margin":15,"y_axis_title":"","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"echarts4Colors","stack":true,"only_total":true,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: ba02528b-184b-4304-b027-f2b7d9011ab0 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_Visits_2.yaml b/dff/config/superset_dashboard/charts/Node_Visits_2.yaml deleted file mode 100644 index 802288e28..000000000 --- a/dff/config/superset_dashboard/charts/Node_Visits_2.yaml +++ /dev/null @@ -1,59 +0,0 @@ -slice_name: Node Visits -description: null -certified_by: null -certification_details: null -viz_type: dist_bar -params: - adhoc_filters: [] - bottom_margin: auto - color_scheme: supersetColors - columns: - - label - datasource: 3__table - extra_form_data: {} - granularity_sqla: start_time - groupby: - - request_id - metrics: - - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 20 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_l2mle87zvnb_nfqzdxmig1d - sqlExpression: null - order_desc: true - rich_tooltip: true - row_limit: 10000 - show_legend: true - time_range: No filter - viz_type: dist_bar - x_axis_label: History id - x_ticks_layout: auto - y_axis_bounds: - - null - - null - y_axis_format: SMART_NUMBER - y_axis_label: Node visits -query_context: null -cache_timeout: null -uuid: 44f4ab9d-5072-4926-a6ed-8615fb81b3d0 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_Visits_7.yaml b/dff/config/superset_dashboard/charts/Node_Visits_7.yaml new file mode 100644 index 000000000..a0485c738 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Node_Visits_7.yaml @@ -0,0 +1,89 @@ +slice_name: Node Visits +description: null +certified_by: null +certification_details: null +viz_type: dist_bar +params: + datasource: 2__table + viz_type: dist_bar + slice_id: 6 + granularity_sqla: start_time + time_range: No filter + metrics: + - aggregate: COUNT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 20 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT(context_id) + optionName: metric_l2mle87zvnb_nfqzdxmig1d + sqlExpression: null + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_jejo4gd8be_f4ygcxhh98w + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_wa9z5rxxr2_b9ez0nkk5le + groupby: + - request_id + columns: + - label + row_limit: 10000 + order_desc: true + color_scheme: echarts4Colors + show_legend: true + rich_tooltip: true + bar_stacked: true + y_axis_format: SMART_NUMBER + y_axis_label: Node visits + y_axis_bounds: + - null + - null + x_axis_label: Dialog turn + bottom_margin: auto + x_ticks_layout: auto + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["request_id","label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":20,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_l2mle87zvnb_nfqzdxmig1d","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"dist_bar","slice_id":6,"granularity_sqla":"start_time","time_range":"No + filter","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":20,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_l2mle87zvnb_nfqzdxmig1d","sqlExpression":null}],"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_jejo4gd8be_f4ygcxhh98w"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_wa9z5rxxr2_b9ez0nkk5le"}],"groupby":["request_id"],"columns":["label"],"row_limit":10000,"order_desc":true,"color_scheme":"echarts4Colors","show_legend":true,"rich_tooltip":true,"bar_stacked":true,"y_axis_format":"SMART_NUMBER","y_axis_label":"Node + visits","y_axis_bounds":[null,null],"x_axis_label":"Dialog turn","bottom_margin":"auto","x_ticks_layout":"auto","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 44f4ab9d-5072-4926-a6ed-8615fb81b3d0 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_counts_3.yaml b/dff/config/superset_dashboard/charts/Node_counts_3.yaml index 8cf476c2f..5d568c3de 100644 --- a/dff/config/superset_dashboard/charts/Node_counts_3.yaml +++ b/dff/config/superset_dashboard/charts/Node_counts_3.yaml @@ -4,17 +4,11 @@ certified_by: null certification_details: null viz_type: dist_bar params: - adhoc_filters: [] - bar_stacked: true - bottom_margin: auto - color_scheme: supersetColors - columns: - - flow_label - datasource: 1__table - extra_form_data: {} + datasource: 2__table + viz_type: dist_bar + slice_id: 9 granularity_sqla: start_time - groupby: - - label + time_range: No filter metrics: - aggregate: COUNT_DISTINCT column: @@ -40,19 +34,53 @@ params: label: COUNT_DISTINCT(context_id) optionName: metric_axee7fzlpu_upud0bdjv6 sqlExpression: null - order_bars: true - order_desc: true - rich_tooltip: true + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_vhz5xz5fww_dgbx6adq7e + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_s3zhicnn9x_9xljjdrli2 + groupby: + - label + columns: + - flow_label row_limit: 10000 + order_desc: true + color_scheme: echarts4Colors show_legend: true - time_range: No filter - viz_type: dist_bar - x_ticks_layout: auto + rich_tooltip: true + bar_stacked: true + order_bars: true + y_axis_format: SMART_NUMBER y_axis_bounds: - null - null - y_axis_format: SMART_NUMBER -query_context: null + bottom_margin: auto + x_ticks_layout: auto + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["label","flow_label"],"metrics":[{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_axee7fzlpu_upud0bdjv6","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"dist_bar","slice_id":9,"granularity_sqla":"start_time","time_range":"No + filter","metrics":[{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_axee7fzlpu_upud0bdjv6","sqlExpression":null}],"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_vhz5xz5fww_dgbx6adq7e"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_s3zhicnn9x_9xljjdrli2"}],"groupby":["label"],"columns":["flow_label"],"row_limit":10000,"order_desc":true,"color_scheme":"echarts4Colors","show_legend":true,"rich_tooltip":true,"bar_stacked":true,"order_bars":true,"y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"bottom_margin":"auto","x_ticks_layout":"auto","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' cache_timeout: null uuid: 0c47c7b5-f500-46cb-97e3-9ebb637f0c8a version: 1.0.0 diff --git a/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_4.yaml b/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_4.yaml deleted file mode 100644 index 93c1bf239..000000000 --- a/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_4.yaml +++ /dev/null @@ -1,86 +0,0 @@ -slice_name: Node visit ratio monitor -description: null -certified_by: null -certification_details: null -viz_type: echarts_timeseries_bar -params: - datasource: 2__table - viz_type: echarts_timeseries_bar - slice_id: 4 - granularity_sqla: start_time - time_grain_sqla: null - time_range: No filter - metrics: - - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_9yefk3wj2g_5wbp61n0pyr - sqlExpression: null - groupby: - - flow_label - - node_label - contributionMode: column - adhoc_filters: [] - order_desc: true - row_limit: 10000 - truncate_metric: true - show_empty_columns: true - comparison_type: values - annotation_layers: [] - forecastPeriods: 10 - forecastInterval: 0.8 - orientation: vertical - x_axis_title: Datetime - x_axis_title_margin: 30 - y_axis_title: Node visit ratio - y_axis_title_margin: 50 - y_axis_title_position: Left - color_scheme: supersetColors - stack: true - only_total: true - zoomable: true - show_legend: true - legendType: scroll - legendOrientation: top - x_axis_time_format: smart_date - xAxisLabelRotation: 45 - y_axis_format: SMART_NUMBER - logAxis: false - minorSplitLine: false - truncateYAxis: false - y_axis_bounds: - - null - - null - rich_tooltip: true - tooltipSortByMetric: true - tooltipTimeFormat: smart_date - extra_form_data: {} - dashboards: - - 1 -query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No - filter","granularity":"start_time","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label","node_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label","node_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":4,"granularity_sqla":"start_time","time_grain_sqla":null,"time_range":"No - filter","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"groupby":["flow_label","node_label"],"contributionMode":"column","adhoc_filters":[],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title":"Datetime","x_axis_title_margin":30,"y_axis_title":"Node - visit ratio","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"supersetColors","stack":true,"only_total":true,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","xAxisLabelRotation":45,"y_axis_format":"SMART_NUMBER","logAxis":false,"minorSplitLine":false,"truncateYAxis":false,"y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipSortByMetric":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' -cache_timeout: null -uuid: 6fafe59c-0fec-4cd8-a8b3-c0bfaffb2135 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml b/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml new file mode 100644 index 000000000..7f5852930 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml @@ -0,0 +1,109 @@ +slice_name: Node visit ratio monitor +description: null +certified_by: null +certification_details: null +viz_type: echarts_timeseries_bar +params: + datasource: 2__table + viz_type: echarts_timeseries_bar + slice_id: 2 + granularity_sqla: start_time + time_grain_sqla: PT1M + time_range: No filter + metrics: + - aggregate: COUNT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 1 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT(context_id) + optionName: metric_9yefk3wj2g_5wbp61n0pyr + sqlExpression: null + groupby: + - flow_label + - node_label + contributionMode: column + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_y0hyd1ebac9_flmblpn0ymt + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_tjpviv3vvq_fdqstvdz39m + order_desc: true + row_limit: 10000 + truncate_metric: true + show_empty_columns: true + comparison_type: values + annotation_layers: [] + forecastPeriods: 10 + forecastInterval: 0.8 + orientation: vertical + x_axis_title: Datetime + x_axis_title_margin: 30 + y_axis_title: Node visit ratio + y_axis_title_margin: 50 + y_axis_title_position: Left + color_scheme: echarts4Colors + stack: true + only_total: true + zoomable: true + show_legend: true + legendType: scroll + legendOrientation: top + x_axis_time_format: smart_date + xAxisLabelRotation: 45 + y_axis_format: SMART_NUMBER + logAxis: false + minorSplitLine: false + truncateYAxis: false + y_axis_bounds: + - null + - null + rich_tooltip: true + tooltipSortByMetric: true + tooltipTimeFormat: smart_date + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"DATEADD(DATETIME(\"now\"), + -1, day) : now","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label","node_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label","node_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":2,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"DATEADD(DATETIME(\"now\"), + -1, day) : now","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"groupby":["flow_label","node_label"],"contributionMode":"column","adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_y0hyd1ebac9_flmblpn0ymt"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_tjpviv3vvq_fdqstvdz39m"}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title":"Datetime","x_axis_title_margin":30,"y_axis_title":"Node + visit ratio","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"echarts4Colors","stack":true,"only_total":true,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","xAxisLabelRotation":45,"y_axis_format":"SMART_NUMBER","logAxis":false,"minorSplitLine":false,"truncateYAxis":false,"y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipSortByMetric":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 6fafe59c-0fec-4cd8-a8b3-c0bfaffb2135 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_visits_cloud_5.yaml b/dff/config/superset_dashboard/charts/Node_visits_cloud_5.yaml deleted file mode 100644 index 334bc85c7..000000000 --- a/dff/config/superset_dashboard/charts/Node_visits_cloud_5.yaml +++ /dev/null @@ -1,48 +0,0 @@ -slice_name: Node visits [cloud] -description: null -certified_by: null -certification_details: null -viz_type: word_cloud -params: - adhoc_filters: [] - color_scheme: supersetColors - datasource: 1__table - extra_form_data: {} - granularity_sqla: start_time - metric: - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_mmbslhy6cnd_6zv1lh26whx - sqlExpression: null - rotation: flat - row_limit: 500 - series: label - size_from: 10 - size_to: 80 - time_range: No filter - viz_type: word_cloud -query_context: null -cache_timeout: null -uuid: b25b4292-ff21-4164-98ac-b1cba95e2994 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_visits_ratio_6.yaml b/dff/config/superset_dashboard/charts/Node_visits_ratio_6.yaml index f3d71f8e9..939773692 100644 --- a/dff/config/superset_dashboard/charts/Node_visits_ratio_6.yaml +++ b/dff/config/superset_dashboard/charts/Node_visits_ratio_6.yaml @@ -4,23 +4,14 @@ certified_by: null certification_details: null viz_type: pie params: - adhoc_filters: [] - color_scheme: supersetColors - datasource: 1__table - date_format: smart_date - donut: true - extra_form_data: {} + datasource: 2__table + viz_type: pie + slice_id: 3 granularity_sqla: start_time + time_range: No filter groupby: - flow_label - node_label - innerRadius: 36 - label_line: false - label_type: key - labels_outside: true - legendMargin: 100 - legendOrientation: top - legendType: plain metric: aggregate: COUNT column: @@ -46,19 +37,54 @@ params: label: COUNT(context_id) optionName: metric_lk827d91wws_mq94n624y6 sqlExpression: null - number_format: SMART_NUMBER - outerRadius: 70 + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_cikhjlv8d6v_qtfsor113yg + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_jnqs9l8t2wl_gn1f9vyfaeh row_limit: 100 - show_labels: true + sort_by_metric: true + color_scheme: echarts4Colors show_labels_threshold: 5 show_legend: true + legendType: plain + legendOrientation: top + legendMargin: 100 + label_type: key + number_format: SMART_NUMBER + date_format: smart_date + show_labels: true + labels_outside: true + label_line: false show_total: true - sort_by_metric: true - time_range: No filter - viz_type: pie + outerRadius: 70 + donut: true + innerRadius: 36 + extra_form_data: {} + dashboards: + - 1 query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No - filter","granularity":"start_time","filters":[],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null},false]],"annotation_layers":[],"row_limit":100,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"adhoc_filters":[],"color_scheme":"supersetColors","datasource":"2__table","date_format":"smart_date","donut":true,"extra_form_data":{},"granularity_sqla":"start_time","groupby":["flow_label","node_label"],"innerRadius":36,"label_line":false,"label_type":"key","labels_outside":true,"legendMargin":100,"legendOrientation":"top","legendType":"plain","metric":{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null},"number_format":"SMART_NUMBER","outerRadius":70,"row_limit":100,"show_labels":true,"show_labels_threshold":5,"show_legend":true,"show_total":true,"slice_id":6,"sort_by_metric":true,"time_range":"No - filter","viz_type":"pie","force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null},false]],"annotation_layers":[],"row_limit":100,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"pie","slice_id":3,"granularity_sqla":"start_time","time_range":"No + filter","groupby":["flow_label","node_label"],"metric":{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_lk827d91wws_mq94n624y6","sqlExpression":null},"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_cikhjlv8d6v_qtfsor113yg"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_jnqs9l8t2wl_gn1f9vyfaeh"}],"row_limit":100,"sort_by_metric":true,"color_scheme":"echarts4Colors","show_labels_threshold":5,"show_legend":true,"legendType":"plain","legendOrientation":"top","legendMargin":100,"label_type":"key","number_format":"SMART_NUMBER","date_format":"smart_date","show_labels":true,"labels_outside":true,"label_line":false,"show_total":true,"outerRadius":70,"donut":true,"innerRadius":36,"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' cache_timeout: null uuid: f9fb7893-3533-4519-bbc4-1f4853f380e1 version: 1.0.0 diff --git a/dff/config/superset_dashboard/charts/Node_visits_sunburst_5.yaml b/dff/config/superset_dashboard/charts/Node_visits_sunburst_5.yaml new file mode 100644 index 000000000..f39969b18 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Node_visits_sunburst_5.yaml @@ -0,0 +1,76 @@ +slice_name: Node visits [sunburst] +description: null +certified_by: null +certification_details: null +viz_type: sunburst +params: + datasource: 2__table + viz_type: sunburst + slice_id: 10 + granularity_sqla: start_time + time_range: No filter + groupby: + - flow_label + - node_label + metric: + aggregate: COUNT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 1 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT(context_id) + optionName: metric_7mo9cnw40ph_rzxhx01jm0c + sqlExpression: null + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_k9appc49vwn_3maud61tywi + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_6s6c2atm24m_3fhzyn98hbs + row_limit: 10000 + color_scheme: echarts4Colors + linear_color_scheme: superset_seq_1 + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''')"},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_7mo9cnw40ph_rzxhx01jm0c","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"sunburst","slice_id":10,"granularity_sqla":"start_time","time_range":"No + filter","groupby":["flow_label","node_label"],"metric":{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_7mo9cnw40ph_rzxhx01jm0c","sqlExpression":null},"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_k9appc49vwn_3maud61tywi"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_6s6c2atm24m_3fhzyn98hbs"}],"row_limit":10000,"color_scheme":"echarts4Colors","linear_color_scheme":"superset_seq_1","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: e955f48c-824c-423c-a11c-5b5dca162927 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Node_visits_sunburst_7.yaml b/dff/config/superset_dashboard/charts/Node_visits_sunburst_7.yaml deleted file mode 100644 index b3293a635..000000000 --- a/dff/config/superset_dashboard/charts/Node_visits_sunburst_7.yaml +++ /dev/null @@ -1,69 +0,0 @@ -slice_name: Node visits [sunburst] -description: null -certified_by: null -certification_details: null -viz_type: sunburst -params: - adhoc_filters: - - clause: WHERE - comparator: null - expressionType: SIMPLE - filterOptionName: filter_82x0yx6fkpv_6h8a2190nmh - isExtra: false - isNew: false - operator: IS NOT NULL - operatorId: IS_NOT_NULL - sqlExpression: null - subject: flow_label - - clause: WHERE - comparator: null - expressionType: SIMPLE - filterOptionName: filter_653tn7jqmox_x65gwtz29gc - isExtra: false - isNew: false - operator: IS NOT NULL - operatorId: IS_NOT_NULL - sqlExpression: null - subject: node_label - color_scheme: supersetColors - datasource: 3__table - extra_form_data: {} - granularity_sqla: start_time - groupby: - - flow_label - - node_label - linear_color_scheme: superset_seq_1 - metric: - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_7mo9cnw40ph_rzxhx01jm0c - sqlExpression: null - row_limit: 10000 - slice_id: 8 - time_range: No filter - viz_type: sunburst -query_context: null -cache_timeout: null -uuid: e955f48c-824c-423c-a11c-5b5dca162927 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Rating_slot_line_chart_1.yaml b/dff/config/superset_dashboard/charts/Rating_slot_line_chart_1.yaml new file mode 100644 index 000000000..3c00dc2ca --- /dev/null +++ b/dff/config/superset_dashboard/charts/Rating_slot_line_chart_1.yaml @@ -0,0 +1,92 @@ +slice_name: Rating slot [line chart] +description: null +certified_by: null +certification_details: null +viz_type: echarts_timeseries_line +params: + datasource: 3__table + viz_type: echarts_timeseries_line + slice_id: 7 + granularity_sqla: start_time + time_grain_sqla: PT1M + time_range: No filter + metrics: + - aggregate: null + column: null + datasourceWarning: false + expressionType: SQL + hasCustomLabel: false + label: AVG(CAST(JSON_VALUE(data, '$.rating') AS... + optionName: metric_00oy6lz4f1x8m_ogwvyq0hxx + sqlExpression: AVG(CAST(JSON_VALUE(data, '$.rating') AS Int16)) + groupby: [] + adhoc_filters: + - clause: WHERE + comparator: get_slots + datasourceWarning: false + expressionType: SIMPLE + filterOptionName: filter_7obcf0exa9_1e58m260tq5 + isExtra: false + isNew: false + operator: == + operatorId: EQUALS + sqlExpression: null + subject: data_key + - clause: WHERE + comparator: null + datasourceWarning: false + expressionType: SQL + filterOptionName: filter_0sg816yobyac_obqceo13prc + isExtra: false + isNew: false + operator: null + sqlExpression: JSON_VALUE(data, '$.rating') <> '' + subject: null + order_desc: true + row_limit: 10000 + truncate_metric: true + show_empty_columns: true + comparison_type: values + annotation_layers: [] + forecastPeriods: 10 + forecastInterval: 0.8 + x_axis_title: Time axis + x_axis_title_margin: 30 + y_axis_title: Average rating + y_axis_title_margin: 30 + y_axis_title_position: Left + color_scheme: echarts4Colors + seriesType: line + only_total: true + opacity: 0.2 + markerSize: 6 + show_legend: true + legendType: scroll + legendOrientation: top + x_axis_time_format: smart_date + rich_tooltip: true + tooltipTimeFormat: smart_date + y_axis_format: SMART_NUMBER + y_axis_bounds: + - null + - null + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_slots"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(JSON_VALUE(data, + ''$.rating'') <> '''')"},"applied_time_extras":{},"columns":[],"metrics":[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS...","optionName":"metric_00oy6lz4f1x8m_ogwvyq0hxx","sqlExpression":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS Int16))"}],"orderby":[[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS...","optionName":"metric_00oy6lz4f1x8m_ogwvyq0hxx","sqlExpression":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS Int16))"},false]],"annotation_layers":[],"row_limit":10000,"series_columns":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":[],"aggregates":{"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS...":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"flatten"}]}],"form_data":{"datasource":"3__table","viz_type":"echarts_timeseries_line","slice_id":7,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"No + filter","metrics":[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS...","optionName":"metric_00oy6lz4f1x8m_ogwvyq0hxx","sqlExpression":"AVG(CAST(JSON_VALUE(data, + ''$.rating'') AS Int16))"}],"groupby":[],"adhoc_filters":[{"clause":"WHERE","comparator":"get_slots","datasourceWarning":false,"expressionType":"SIMPLE","filterOptionName":"filter_7obcf0exa9_1e58m260tq5","isExtra":false,"isNew":false,"operator":"==","operatorId":"EQUALS","sqlExpression":null,"subject":"data_key"},{"clause":"WHERE","comparator":null,"datasourceWarning":false,"expressionType":"SQL","filterOptionName":"filter_0sg816yobyac_obqceo13prc","isExtra":false,"isNew":false,"operator":null,"sqlExpression":"JSON_VALUE(data, + ''$.rating'') <> ''''","subject":null}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"x_axis_title":"Time + axis","x_axis_title_margin":30,"y_axis_title":"Average rating","y_axis_title_margin":30,"y_axis_title_position":"Left","color_scheme":"echarts4Colors","seriesType":"line","only_total":true,"opacity":0.2,"markerSize":6,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","rich_tooltip":true,"tooltipTimeFormat":"smart_date","y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 128e1da2-1efd-433d-9b93-a07de175e2bc +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Requests_17.yaml b/dff/config/superset_dashboard/charts/Requests_17.yaml new file mode 100644 index 000000000..9881f448b --- /dev/null +++ b/dff/config/superset_dashboard/charts/Requests_17.yaml @@ -0,0 +1,59 @@ +slice_name: Requests +description: null +certified_by: null +certification_details: null +viz_type: table +params: + datasource: 3__table + viz_type: table + slice_id: 8 + granularity_sqla: start_time + time_grain_sqla: P1D + time_range: No filter + query_mode: raw + groupby: [] + all_columns: + - context_id + - request_id + - start_time + - expressionType: SQL + label: data + sqlExpression: JSON_VALUE(data, '$.last_request') + datasourceWarning: false + percent_metrics: [] + adhoc_filters: + - clause: WHERE + comparator: get_last_request + datasourceWarning: false + expressionType: SIMPLE + filterOptionName: filter_4amlkzz0hd3_exx83iz5rof + isExtra: false + isNew: false + operator: == + operatorId: EQUALS + sqlExpression: null + subject: data_key + order_by_cols: [] + row_limit: 1000 + server_page_length: 10 + order_desc: true + table_timestamp_format: '%d-%m-%Y %H:%M:%S' + show_cell_bars: true + color_pn: true + column_config: + data: + columnWidth: 150 + truncateLongCells: true + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_last_request"}],"extras":{"time_grain_sqla":"P1D","having":"","where":""},"applied_time_extras":{},"columns":["context_id","request_id","start_time",{"expressionType":"SQL","label":"data","sqlExpression":"JSON_VALUE(data, + ''$.last_request'')","datasourceWarning":false}],"orderby":[],"annotation_layers":[],"row_limit":1000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[]}],"form_data":{"datasource":"3__table","viz_type":"table","slice_id":8,"granularity_sqla":"start_time","time_grain_sqla":"P1D","time_range":"No + filter","query_mode":"raw","groupby":[],"all_columns":["context_id","request_id","start_time",{"expressionType":"SQL","label":"data","sqlExpression":"JSON_VALUE(data, + ''$.last_request'')","datasourceWarning":false}],"percent_metrics":[],"adhoc_filters":[{"clause":"WHERE","comparator":"get_last_request","datasourceWarning":false,"expressionType":"SIMPLE","filterOptionName":"filter_4amlkzz0hd3_exx83iz5rof","isExtra":false,"isNew":false,"operator":"==","operatorId":"EQUALS","sqlExpression":null,"subject":"data_key"}],"order_by_cols":[],"row_limit":1000,"server_page_length":10,"include_time":false,"order_desc":true,"table_timestamp_format":"%d-%m-%Y + %H:%M:%S","show_cell_bars":true,"color_pn":true,"column_config":{"data":{"columnWidth":150,"truncateLongCells":true}},"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 45ef67db-d33d-49f5-8d46-1f0fa518eaac +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Responses_16.yaml b/dff/config/superset_dashboard/charts/Responses_16.yaml new file mode 100644 index 000000000..02349a9f1 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Responses_16.yaml @@ -0,0 +1,59 @@ +slice_name: Responses +description: null +certified_by: null +certification_details: null +viz_type: table +params: + datasource: 3__table + viz_type: table + slice_id: 15 + granularity_sqla: start_time + time_grain_sqla: P1D + time_range: No filter + query_mode: raw + groupby: [] + all_columns: + - context_id + - request_id + - start_time + - expressionType: SQL + label: data + sqlExpression: JSON_VALUE(data, '$.last_response') + datasourceWarning: false + percent_metrics: [] + adhoc_filters: + - clause: WHERE + comparator: get_last_response + datasourceWarning: false + expressionType: SIMPLE + filterOptionName: filter_m3nzyr545di_lxjaivcal1 + isExtra: false + isNew: false + operator: == + operatorId: EQUALS + sqlExpression: null + subject: data_key + order_by_cols: [] + row_limit: 1000 + server_page_length: 10 + order_desc: true + table_timestamp_format: '%d-%m-%Y %H:%M:%S' + show_cell_bars: true + color_pn: true + column_config: + data: + columnWidth: 150 + truncateLongCells: true + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_last_response"}],"extras":{"time_grain_sqla":"P1D","having":"","where":""},"applied_time_extras":{},"columns":["context_id","request_id","start_time",{"expressionType":"SQL","label":"data","sqlExpression":"JSON_VALUE(data, + ''$.last_response'')","datasourceWarning":false}],"orderby":[],"annotation_layers":[],"row_limit":1000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[]}],"form_data":{"datasource":"3__table","viz_type":"table","slice_id":15,"granularity_sqla":"start_time","time_grain_sqla":"P1D","time_range":"No + filter","query_mode":"raw","groupby":[],"all_columns":["context_id","request_id","start_time",{"expressionType":"SQL","label":"data","sqlExpression":"JSON_VALUE(data, + ''$.last_response'')","datasourceWarning":false}],"percent_metrics":[],"adhoc_filters":[{"clause":"WHERE","comparator":"get_last_response","datasourceWarning":false,"expressionType":"SIMPLE","filterOptionName":"filter_m3nzyr545di_lxjaivcal1","isExtra":false,"isNew":false,"operator":"==","operatorId":"EQUALS","sqlExpression":null,"subject":"data_key"}],"order_by_cols":[],"row_limit":1000,"server_page_length":10,"include_time":false,"order_desc":true,"table_timestamp_format":"%d-%m-%Y + %H:%M:%S","show_cell_bars":true,"color_pn":true,"column_config":{"data":{"columnWidth":150,"truncateLongCells":true}},"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 7844eb6d-4fce-44cf-8994-fb61a221a2ab +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Service_load_max_dialogue_length_8.yaml b/dff/config/superset_dashboard/charts/Service_load_max_dialogue_length_8.yaml deleted file mode 100644 index 1e087769a..000000000 --- a/dff/config/superset_dashboard/charts/Service_load_max_dialogue_length_8.yaml +++ /dev/null @@ -1,83 +0,0 @@ -slice_name: Service load [max dialogue length] -description: null -certified_by: null -certification_details: null -viz_type: echarts_timeseries_step -params: - datasource: 2__table - viz_type: echarts_timeseries_step - slice_id: 8 - granularity_sqla: start_time - time_grain_sqla: null - time_range: No filter - metrics: - - aggregate: null - column: null - datasourceWarning: false - expressionType: SQL - hasCustomLabel: false - label: AVG(CAST(request_id AS Int16)) - optionName: metric_8h0zfurdcy_tzo5q0tuczi - sqlExpression: AVG(CAST(request_id AS Int16)) - - aggregate: null - column: null - datasourceWarning: false - expressionType: SQL - hasCustomLabel: false - label: MAX(CAST(request_id AS Int16)) - optionName: metric_wz5zx1kqqc_u42eslame2 - sqlExpression: MAX(CAST(request_id AS Int16)) - groupby: [] - adhoc_filters: [] - order_desc: true - row_limit: 10000 - truncate_metric: true - show_empty_columns: true - comparison_type: values - annotation_layers: [] - forecastPeriods: 10 - forecastInterval: 0.8 - x_axis_title_margin: 15 - y_axis_title: AVG/MAX dialogue length - y_axis_title_margin: 50 - y_axis_title_position: Left - color_scheme: supersetColors - seriesType: start - only_total: true - opacity: 0.2 - markerSize: 6 - zoomable: true - show_legend: true - legendType: scroll - legendOrientation: top - x_axis_time_format: smart_date - rich_tooltip: true - tooltipSortByMetric: false - tooltipTimeFormat: smart_date - y_axis_format: SMART_NUMBER - logAxis: false - y_axis_bounds: - - null - - null - extra_form_data: {} - dashboards: - - 1 -query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No - filter","granularity":"start_time","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(request_id - AS Int16))","optionName":"metric_8h0zfurdcy_tzo5q0tuczi","sqlExpression":"AVG(CAST(request_id - AS Int16))"},{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"MAX(CAST(request_id - AS Int16))","optionName":"metric_wz5zx1kqqc_u42eslame2","sqlExpression":"MAX(CAST(request_id - AS Int16))"}],"orderby":[[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(request_id - AS Int16))","optionName":"metric_8h0zfurdcy_tzo5q0tuczi","sqlExpression":"AVG(CAST(request_id - AS Int16))"},false]],"annotation_layers":[],"row_limit":10000,"series_columns":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":[],"aggregates":{"AVG(CAST(request_id - AS Int16))":{"operator":"mean"},"MAX(CAST(request_id AS Int16))":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_step","slice_id":8,"granularity_sqla":"start_time","time_grain_sqla":null,"time_range":"No - filter","metrics":[{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"AVG(CAST(request_id - AS Int16))","optionName":"metric_8h0zfurdcy_tzo5q0tuczi","sqlExpression":"AVG(CAST(request_id - AS Int16))"},{"aggregate":null,"column":null,"datasourceWarning":false,"expressionType":"SQL","hasCustomLabel":false,"label":"MAX(CAST(request_id - AS Int16))","optionName":"metric_wz5zx1kqqc_u42eslame2","sqlExpression":"MAX(CAST(request_id - AS Int16))"}],"groupby":[],"adhoc_filters":[],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"x_axis_title_margin":15,"y_axis_title":"AVG/MAX - dialogue length","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"supersetColors","seriesType":"start","only_total":true,"opacity":0.2,"markerSize":6,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","rich_tooltip":true,"tooltipSortByMetric":false,"tooltipTimeFormat":"smart_date","y_axis_format":"SMART_NUMBER","logAxis":false,"y_axis_bounds":[null,null],"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' -cache_timeout: null -uuid: 276c3aa7-89bc-49ff-91b6-6232ae35c854 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Table_10.yaml b/dff/config/superset_dashboard/charts/Table_10.yaml deleted file mode 100644 index b3f9f636c..000000000 --- a/dff/config/superset_dashboard/charts/Table_10.yaml +++ /dev/null @@ -1,34 +0,0 @@ -slice_name: Table -description: null -certified_by: null -certification_details: null -viz_type: table -params: - adhoc_filters: [] - all_columns: [] - color_pn: true - datasource: 3__table - extra_form_data: {} - granularity_sqla: start_time - groupby: - - context_id - - start_time - - data_key - - data - order_by_cols: [] - order_desc: false - percent_metrics: [] - query_mode: aggregate - row_limit: 10000 - server_page_length: 10 - show_cell_bars: true - slice_id: 14 - table_timestamp_format: smart_date - time_grain_sqla: null - time_range: No filter - viz_type: table -query_context: null -cache_timeout: null -uuid: 38d353dc-c0ef-41a0-97c7-3dcbebab9e02 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Table_14.yaml b/dff/config/superset_dashboard/charts/Table_14.yaml new file mode 100644 index 000000000..fade8e8c9 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Table_14.yaml @@ -0,0 +1,38 @@ +slice_name: Table +description: null +certified_by: null +certification_details: null +viz_type: table +params: + datasource: 3__table + viz_type: table + slice_id: 16 + granularity_sqla: start_time + time_grain_sqla: null + time_range: No filter + query_mode: aggregate + groupby: + - context_id + - start_time + - data_key + - data + all_columns: [] + percent_metrics: [] + adhoc_filters: [] + order_by_cols: [] + row_limit: 10000 + server_page_length: 10 + order_desc: false + table_timestamp_format: smart_date + show_cell_bars: true + color_pn: true + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":3,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":["context_id","start_time","data_key","data"],"metrics":[],"orderby":[],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":false,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[]}],"form_data":{"datasource":"3__table","viz_type":"table","slice_id":16,"granularity_sqla":"start_time","time_grain_sqla":null,"time_range":"No + filter","query_mode":"aggregate","groupby":["context_id","start_time","data_key","data"],"all_columns":[],"percent_metrics":[],"adhoc_filters":[],"order_by_cols":[],"row_limit":10000,"server_page_length":10,"order_desc":false,"table_timestamp_format":"smart_date","show_cell_bars":true,"color_pn":true,"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 38d353dc-c0ef-41a0-97c7-3dcbebab9e02 +version: 1.0.0 +dataset_uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 diff --git a/dff/config/superset_dashboard/charts/Terminal_labels_11.yaml b/dff/config/superset_dashboard/charts/Terminal_labels_15.yaml similarity index 100% rename from dff/config/superset_dashboard/charts/Terminal_labels_11.yaml rename to dff/config/superset_dashboard/charts/Terminal_labels_15.yaml diff --git a/dff/config/superset_dashboard/charts/Transition_counts_12.yaml b/dff/config/superset_dashboard/charts/Transition_counts_12.yaml index f44c72191..56c6099f9 100644 --- a/dff/config/superset_dashboard/charts/Transition_counts_12.yaml +++ b/dff/config/superset_dashboard/charts/Transition_counts_12.yaml @@ -4,18 +4,11 @@ certified_by: null certification_details: null viz_type: dist_bar params: - adhoc_filters: [] - bar_stacked: true - bottom_margin: auto - color_scheme: supersetColors - columns: - - flow_label - datasource: 1__table - extra_form_data: {} + datasource: 2__table + viz_type: dist_bar + slice_id: 1 granularity_sqla: start_time - groupby: - - prev_label - - label + time_range: No filter metrics: - aggregate: COUNT_DISTINCT column: @@ -41,22 +34,67 @@ params: label: COUNT_DISTINCT(context_id) optionName: metric_tc3lb0a1pff_dyroibp08h7 sqlExpression: null - order_desc: true - rich_tooltip: true + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_f7v0qac48fr_l37sg1wvtm + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_e9h83eu2wg_ata0o82djjn + - expressionType: SQL + sqlExpression: prev_label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_t2quebiqkp_x1qhr9mtq7f + groupby: + - prev_label + - label + columns: + - flow_label row_limit: 10000 + order_desc: true + color_scheme: echarts4Colors show_legend: true - slice_id: 6 - time_range: No filter - viz_type: dist_bar - x_axis_label: Transitions - x_ticks_layout: auto - y_axis_bounds: - - null - - null + rich_tooltip: true + bar_stacked: true y_axis_format: SMART_NUMBER y_axis_label: Counts y_axis_showminmax: false -query_context: null + y_axis_bounds: + - null + - null + x_axis_label: Transitions + bottom_margin: auto + x_ticks_layout: auto + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''') AND (prev_label <> '''')"},"applied_time_extras":{},"columns":["prev_label","label","flow_label"],"metrics":[{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_tc3lb0a1pff_dyroibp08h7","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"dist_bar","slice_id":1,"granularity_sqla":"start_time","time_range":"No + filter","metrics":[{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_tc3lb0a1pff_dyroibp08h7","sqlExpression":null}],"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_f7v0qac48fr_l37sg1wvtm"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_e9h83eu2wg_ata0o82djjn"},{"expressionType":"SQL","sqlExpression":"prev_label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_t2quebiqkp_x1qhr9mtq7f"}],"groupby":["prev_label","label"],"columns":["flow_label"],"row_limit":10000,"order_desc":true,"color_scheme":"echarts4Colors","show_legend":true,"rich_tooltip":true,"bar_stacked":true,"y_axis_format":"SMART_NUMBER","y_axis_label":"Counts","y_axis_showminmax":false,"y_axis_bounds":[null,null],"x_axis_label":"Transitions","bottom_margin":"auto","x_ticks_layout":"auto","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' cache_timeout: null uuid: 9fcc7cc1-9257-4c0d-b377-b3a60a8bf3df version: 1.0.0 diff --git a/dff/config/superset_dashboard/charts/Transition_layout_10.yaml b/dff/config/superset_dashboard/charts/Transition_layout_10.yaml new file mode 100644 index 000000000..8b674fd59 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Transition_layout_10.yaml @@ -0,0 +1,101 @@ +slice_name: Transition layout +description: null +certified_by: null +certification_details: null +viz_type: graph_chart +params: + datasource: 2__table + viz_type: graph_chart + slice_id: 8 + granularity_sqla: start_time + time_range: No filter + source: prev_label + target: label + metric: + aggregate: COUNT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 1 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT(context_id) + optionName: metric_qxsyaujh63_4b75kyx6b5g + sqlExpression: null + source_category: prev_flow + target_category: flow_label + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_avle3r80zu4_c4684cbrjfg + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_0jip6y1w3awu_eysm19dnp2 + - expressionType: SQL + sqlExpression: prev_label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_29oq8kbalhr_6wudwxysro + row_limit: 10000 + color_scheme: echarts4Colors + show_legend: true + legendType: scroll + legendOrientation: top + layout: force + edgeSymbol: none,arrow + draggable: false + roam: scale + selectedMode: single + baseNodeSize: 20 + baseEdgeWidth: 3 + edgeLength: 400 + gravity: 0.3 + repulsion: 1000 + friction: 0.2 + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''') AND (prev_label <> '''')"},"applied_time_extras":{},"columns":["prev_label","label","flow_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_qxsyaujh63_4b75kyx6b5g","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"graph_chart","slice_id":8,"granularity_sqla":"start_time","time_range":"No + filter","source":"prev_label","target":"label","metric":{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_qxsyaujh63_4b75kyx6b5g","sqlExpression":null},"source_category":"prev_flow","target_category":"flow_label","adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_avle3r80zu4_c4684cbrjfg"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_0jip6y1w3awu_eysm19dnp2"},{"expressionType":"SQL","sqlExpression":"prev_label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_29oq8kbalhr_6wudwxysro"}],"row_limit":10000,"color_scheme":"echarts4Colors","show_legend":true,"legendType":"scroll","legendOrientation":"top","layout":"force","edgeSymbol":"none,arrow","draggable":false,"roam":"scale","selectedMode":"single","baseNodeSize":20,"baseEdgeWidth":3,"edgeLength":400,"gravity":0.3,"repulsion":1000,"friction":0.2,"extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 801e8c66-f693-46e3-a9a5-7b2b0b08a4a7 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Transition_layout_13.yaml b/dff/config/superset_dashboard/charts/Transition_layout_13.yaml deleted file mode 100644 index 5999b30ed..000000000 --- a/dff/config/superset_dashboard/charts/Transition_layout_13.yaml +++ /dev/null @@ -1,62 +0,0 @@ -slice_name: Transition layout -description: null -certified_by: null -certification_details: null -viz_type: graph_chart -params: - adhoc_filters: [] - baseEdgeWidth: 3 - baseNodeSize: 20 - color_scheme: supersetColors - datasource: 1__table - draggable: false - edgeLength: 400 - edgeSymbol: none,arrow - extra_form_data: {} - friction: 0.2 - granularity_sqla: start_time - gravity: 0.3 - layout: force - legendOrientation: top - legendType: scroll - metric: - aggregate: COUNT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT(context_id) - optionName: metric_qxsyaujh63_4b75kyx6b5g - sqlExpression: null - repulsion: 1000 - roam: scale - row_limit: 10000 - selectedMode: single - show_legend: true - source: prev_label - source_category: flow_label - target: label - target_category: flow_label - time_range: No filter - viz_type: graph_chart -query_context: null -cache_timeout: null -uuid: 801e8c66-f693-46e3-a9a5-7b2b0b08a4a7 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Transition_ratio_chord_11.yaml b/dff/config/superset_dashboard/charts/Transition_ratio_chord_11.yaml new file mode 100644 index 000000000..5083aa859 --- /dev/null +++ b/dff/config/superset_dashboard/charts/Transition_ratio_chord_11.yaml @@ -0,0 +1,86 @@ +slice_name: Transition ratio [chord] +description: null +certified_by: null +certification_details: null +viz_type: chord +params: + datasource: 2__table + viz_type: chord + slice_id: 12 + granularity_sqla: start_time + time_range: No filter + groupby: label + columns: prev_label + metric: + aggregate: COUNT_DISTINCT + column: + advanced_data_type: null + certification_details: null + certified_by: null + column_name: context_id + description: null + expression: null + filterable: true + groupby: true + id: 1 + is_certified: false + is_dttm: false + python_date_format: null + type: STRING + type_generic: 1 + verbose_name: null + warning_markdown: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: COUNT_DISTINCT(context_id) + optionName: metric_97lz5hqft8j_aqofzpt1ma5 + sqlExpression: null + adhoc_filters: + - expressionType: SIMPLE + subject: data_key + operator: == + operatorId: EQUALS + comparator: get_current_label + clause: WHERE + sqlExpression: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_ug6txpdjuzj_znl0zrda72n + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_a4j5xkh81f5_k2i0586j12h + - expressionType: SQL + sqlExpression: prev_label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_p4zwr1yr4v9_avka1ppurai + row_limit: 10000 + y_axis_format: ~g + color_scheme: echarts4Colors + extra_form_data: {} + dashboards: + - 1 +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"having":"","where":"(label + <> '''') AND (prev_label <> '''')"},"applied_time_extras":{},"columns":["label","prev_label"],"metrics":[{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_97lz5hqft8j_aqofzpt1ma5","sqlExpression":null}],"annotation_layers":[],"row_limit":10000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"datasource":"2__table","viz_type":"chord","slice_id":12,"granularity_sqla":"start_time","time_range":"No + filter","groupby":"label","columns":"prev_label","metric":{"aggregate":"COUNT_DISTINCT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT_DISTINCT(context_id)","optionName":"metric_97lz5hqft8j_aqofzpt1ma5","sqlExpression":null},"adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_ug6txpdjuzj_znl0zrda72n"},{"expressionType":"SQL","sqlExpression":"label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_a4j5xkh81f5_k2i0586j12h"},{"expressionType":"SQL","sqlExpression":"prev_label + <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_p4zwr1yr4v9_avka1ppurai"}],"row_limit":10000,"y_axis_format":"~g","color_scheme":"echarts4Colors","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' +cache_timeout: null +uuid: 388d2359-8d13-4795-8dc9-1cb5dfa92ee1 +version: 1.0.0 +dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/charts/Transition_ratio_chord_14.yaml b/dff/config/superset_dashboard/charts/Transition_ratio_chord_14.yaml deleted file mode 100644 index e9d1f8723..000000000 --- a/dff/config/superset_dashboard/charts/Transition_ratio_chord_14.yaml +++ /dev/null @@ -1,48 +0,0 @@ -slice_name: Transition ratio [chord] -description: null -certified_by: null -certification_details: null -viz_type: chord -params: - adhoc_filters: [] - color_scheme: bnbColors - columns: prev_label - datasource: 1__table - extra_form_data: {} - granularity_sqla: start_time - groupby: label - metric: - aggregate: COUNT_DISTINCT - column: - advanced_data_type: null - certification_details: null - certified_by: null - column_name: context_id - description: null - expression: null - filterable: true - groupby: true - id: 1 - is_certified: false - is_dttm: false - python_date_format: null - type: STRING - type_generic: 1 - verbose_name: null - warning_markdown: null - expressionType: SIMPLE - hasCustomLabel: false - isNew: false - label: COUNT_DISTINCT(context_id) - optionName: metric_97lz5hqft8j_aqofzpt1ma5 - sqlExpression: null - row_limit: 10000 - slice_id: 13 - time_range: No filter - viz_type: chord - y_axis_format: ~g -query_context: null -cache_timeout: null -uuid: 388d2359-8d13-4795-8dc9-1cb5dfa92ee1 -version: 1.0.0 -dataset_uuid: fda98ab8-f550-45f1-9ded-0113f3e67260 diff --git a/dff/config/superset_dashboard/dashboards/DFF_Stats_1.yaml b/dff/config/superset_dashboard/dashboards/DFF_Stats_1.yaml deleted file mode 100644 index 0523b8ae1..000000000 --- a/dff/config/superset_dashboard/dashboards/DFF_Stats_1.yaml +++ /dev/null @@ -1,555 +0,0 @@ -dashboard_title: DFF Stats -description: null -css: '' -slug: dff-stats -uuid: 68bce374-99bc-4890-b8c2-cb172409b894 -position: - CHART-Af3zvLsiKV: - children: [] - id: CHART-Af3zvLsiKV - meta: - chartId: 4 - height: 66 - sliceName: Node visit ratio monitor - uuid: 6fafe59c-0fec-4cd8-a8b3-c0bfaffb2135 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - - ROW-7g6_n72hZ - type: CHART - CHART-CuCYDOlGEu: - children: [] - id: CHART-CuCYDOlGEu - meta: - chartId: 10 - height: 50 - sliceName: Table - sliceNameOverride: Stats table - uuid: 38d353dc-c0ef-41a0-97c7-3dcbebab9e02 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-JCU6rANFP - - ROW-EaYAQjv4W - type: CHART - CHART-DxY0c3RnIv: - children: [] - id: CHART-DxY0c3RnIv - meta: - chartId: 3 - height: 61 - sliceName: Node counts - uuid: 0c47c7b5-f500-46cb-97e3-9ebb637f0c8a - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-VUknWnOAy - - ROW-PvToekFjO - type: CHART - CHART-explore-10-1: - children: [] - id: CHART-explore-10-1 - meta: - chartId: 13 - height: 73 - sliceName: Transition layout - sliceNameOverride: Dialogue layout - uuid: 801e8c66-f693-46e3-a9a5-7b2b0b08a4a7 - width: 6 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-JCU6rANFP - - ROW-Ccs1FbcI- - type: CHART - CHART-explore-11-1: - children: [] - id: CHART-explore-11-1 - meta: - chartId: 5 - height: 56 - sliceName: Node visits [cloud] - uuid: b25b4292-ff21-4164-98ac-b1cba95e2994 - width: 8 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - - ROW-rR4J0eAhh - type: CHART - CHART-explore-13-1: - children: [] - id: CHART-explore-13-1 - meta: - chartId: 14 - height: 74 - sliceName: Transition ratio [chord] - uuid: 388d2359-8d13-4795-8dc9-1cb5dfa92ee1 - width: 6 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-JCU6rANFP - - ROW-Ccs1FbcI- - type: CHART - CHART-explore-14-1: - children: [] - id: CHART-explore-14-1 - meta: - chartId: 7 - height: 105 - sliceName: Node visits [sunburst] - uuid: e955f48c-824c-423c-a11c-5b5dca162927 - width: 6 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - - ROW-ZcN9G9RL5 - type: CHART - CHART-explore-20-1: - children: [] - id: CHART-explore-20-1 - meta: - chartId: 2 - height: 50 - sliceName: Node Visits - uuid: 44f4ab9d-5072-4926-a6ed-8615fb81b3d0 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - - ROW-ksepbeQu6 - type: CHART - CHART-explore-21-1: - children: [] - id: CHART-explore-21-1 - meta: - chartId: 1 - height: 60 - sliceName: Flow visit ratio monitor - uuid: ba02528b-184b-4304-b027-f2b7d9011ab0 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - - ROW-ZTVWOu2o0 - type: CHART - CHART-explore-22-1: - children: [] - id: CHART-explore-22-1 - meta: - chartId: 11 - height: 61 - sliceName: Terminal labels - sliceNameOverride: Terminal labels monitor - uuid: cf066e41-a9e8-4f54-a875-ebf4da350b59 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - - ROW-LhKWfqM7V - type: CHART - CHART-explore-9-1: - children: [] - id: CHART-explore-9-1 - meta: - chartId: 6 - height: 105 - sliceName: Node visits [ratio] - uuid: f9fb7893-3533-4519-bbc4-1f4853f380e1 - width: 6 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - - ROW-ZcN9G9RL5 - type: CHART - CHART-mYC2udeF3a: - children: [] - id: CHART-mYC2udeF3a - meta: - chartId: 9 - height: 50 - sliceName: Service load [users] - uuid: b5d43314-514c-464e-9fcc-f897e3ae0963 - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - - ROW-Ae7v1prsp2 - type: CHART - CHART-wci7CHiza6: - children: [] - id: CHART-wci7CHiza6 - meta: - chartId: 12 - height: 66 - sliceName: Transition counts - uuid: 9fcc7cc1-9257-4c0d-b377-b3a60a8bf3df - width: 12 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-VUknWnOAy - - ROW-TRNUnY9X_Y - type: CHART - COLUMN-JRaDi96UVP: - children: [] - id: COLUMN-JRaDi96UVP - meta: - background: BACKGROUND_TRANSPARENT - width: 2 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - - ROW-rR4J0eAhh - type: COLUMN - COLUMN-WysgPuf0P1: - children: [] - id: COLUMN-WysgPuf0P1 - meta: - background: BACKGROUND_TRANSPARENT - width: 2 - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - - ROW-rR4J0eAhh - type: COLUMN - DASHBOARD_VERSION_KEY: v2 - GRID_ID: - children: - - HEADER-nYVwogYInk - - TABS-Xoi5oUBxZI - id: GRID_ID - parents: - - ROOT_ID - type: GRID - HEADER-nYVwogYInk: - children: [] - id: HEADER-nYVwogYInk - meta: - background: BACKGROUND_TRANSPARENT - headerSize: LARGE_HEADER - text: DFF Stats - parents: - - ROOT_ID - - GRID_ID - type: HEADER - HEADER_ID: - id: HEADER_ID - meta: - text: DFF Stats - type: HEADER - ROOT_ID: - children: - - GRID_ID - id: ROOT_ID - type: ROOT - ROW-7g6_n72hZ: - children: - - CHART-Af3zvLsiKV - id: ROW-7g6_n72hZ - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - type: ROW - ROW-Ae7v1prsp2: - children: - - CHART-mYC2udeF3a - id: ROW-Ae7v1prsp2 - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - type: ROW - ROW-Ccs1FbcI-: - children: - - CHART-explore-10-1 - - CHART-explore-13-1 - id: ROW-Ccs1FbcI- - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-JCU6rANFP - type: ROW - ROW-EaYAQjv4W: - children: - - CHART-CuCYDOlGEu - id: ROW-EaYAQjv4W - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-JCU6rANFP - type: ROW - ROW-LhKWfqM7V: - children: - - CHART-explore-22-1 - id: ROW-LhKWfqM7V - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - type: ROW - ROW-PvToekFjO: - children: - - CHART-DxY0c3RnIv - id: ROW-PvToekFjO - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-VUknWnOAy - type: ROW - ROW-TRNUnY9X_Y: - children: - - CHART-wci7CHiza6 - id: ROW-TRNUnY9X_Y - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-VUknWnOAy - type: ROW - ROW-ZTVWOu2o0: - children: - - CHART-explore-21-1 - id: ROW-ZTVWOu2o0 - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - type: ROW - ROW-ZcN9G9RL5: - children: - - CHART-explore-9-1 - - CHART-explore-14-1 - id: ROW-ZcN9G9RL5 - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - type: ROW - ROW-hfJk2ddHi: - children: - - CHART-explore-20-1 - id: ROW-hfJk2ddHi - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-VUknWnOAy - type: ROW - ROW-ksepbeQu6: - children: - - CHART-explore-20-1 - id: ROW-ksepbeQu6 - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-6zE8noCIsx - type: ROW - ROW-rR4J0eAhh: - children: - - COLUMN-JRaDi96UVP - - CHART-explore-11-1 - - COLUMN-WysgPuf0P1 - id: ROW-rR4J0eAhh - meta: - background: BACKGROUND_TRANSPARENT - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - - TAB-Gw0Ffh1lG - type: ROW - TAB-6zE8noCIsx: - children: - - ROW-Ae7v1prsp2 - - ROW-ksepbeQu6 - - ROW-ZTVWOu2o0 - - ROW-7g6_n72hZ - - ROW-LhKWfqM7V - id: TAB-6zE8noCIsx - meta: - defaultText: Tab title - placeholder: Tab title - text: Service stats - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - type: TAB - TAB-Gw0Ffh1lG: - children: - - ROW-ZcN9G9RL5 - - ROW-rR4J0eAhh - id: TAB-Gw0Ffh1lG - meta: - defaultText: Tab title - placeholder: Tab title - text: General stats - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - type: TAB - TAB-JCU6rANFP: - children: - - ROW-EaYAQjv4W - - ROW-Ccs1FbcI- - id: TAB-JCU6rANFP - meta: - defaultText: Tab title - placeholder: Tab title - text: Overview - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - type: TAB - TAB-VUknWnOAy: - children: - - ROW-PvToekFjO - - ROW-hfJk2ddHi - - ROW-TRNUnY9X_Y - id: TAB-VUknWnOAy - meta: - defaultText: Tab title - placeholder: Tab title - text: Additional stats - parents: - - ROOT_ID - - GRID_ID - - TABS-Xoi5oUBxZI - type: TAB - TABS-Xoi5oUBxZI: - children: - - TAB-JCU6rANFP - - TAB-Gw0Ffh1lG - - TAB-VUknWnOAy - - TAB-6zE8noCIsx - id: TABS-Xoi5oUBxZI - meta: {} - parents: - - ROOT_ID - - GRID_ID - type: TABS -metadata: - color_scheme: echarts4Colors - label_colors: {} - shared_label_colors: - 'greeting_flow: node1': '#c23531' - 'greeting_flow: node2': '#2f4554' - 'greeting_flow: node3': '#61a0a8' - 'greeting_flow: node4': '#d48265' - greeting_flow, node1: '#c23531' - greeting_flow, node2: '#2f4554' - greeting_flow, node3: '#61a0a8' - greeting_flow, node4: '#d48265' - greeting_flow: '#c23531' - AVG(CAST(request_id AS Int16)): '#c23531' - MAX(CAST(request_id AS Int16)): '#2f4554' - root: '#c23531' - node2: '#61a0a8' - node4: '#d48265' - node3: '#91c7ae' - node1: '#749f83' - ? '' - : '#c23531' - timed_refresh_immune_slices: [] - expanded_slices: {} - refresh_frequency: 1800 - show_native_filters: true - default_filters: '{}' - chart_configuration: {} - color_scheme_domain: - - '#c23531' - - '#2f4554' - - '#61a0a8' - - '#d48265' - - '#91c7ae' - - '#749f83' - - '#ca8622' - - '#bda29a' - - '#6e7074' - - '#546570' - - '#c4ccd3' - cross_filters_enabled: false - native_filter_configuration: - - id: NATIVE_FILTER-5vyW3SgU5 - controlValues: - enableEmptyFilter: false - name: Time grain - filterType: filter_timegrain - targets: - - datasetUuid: fda98ab8-f550-45f1-9ded-0113f3e67260 - defaultDataMask: - extraFormData: {} - filterState: {} - ownState: {} - cascadeParentIds: [] - scope: - rootPath: - - TAB-6zE8noCIsx - excluded: - - 7 - type: NATIVE_FILTER - description: '' -version: 1.0.0 diff --git a/dff/config/superset_dashboard/dashboards/DFF_statistics_dashboard_1.yaml b/dff/config/superset_dashboard/dashboards/DFF_statistics_dashboard_1.yaml new file mode 100644 index 000000000..ce8e32496 --- /dev/null +++ b/dff/config/superset_dashboard/dashboards/DFF_statistics_dashboard_1.yaml @@ -0,0 +1,1291 @@ +dashboard_title: DFF statistics dashboard +description: null +css: '' +slug: dff-stats +uuid: 68bce374-99bc-4890-b8c2-cb172409b894 +position: + CHART-91whs_IaiF: + children: [] + id: CHART-91whs_IaiF + meta: + chartId: 17 + height: 50 + sliceName: Requests + uuid: 45ef67db-d33d-49f5-8d46-1f0fa518eaac + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-oXaTD35jB + type: CHART + CHART-Af3zvLsiKV: + children: [] + id: CHART-Af3zvLsiKV + meta: + chartId: 8 + height: 66 + sliceName: Node visit ratio monitor + uuid: 6fafe59c-0fec-4cd8-a8b3-c0bfaffb2135 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-7g6_n72hZ + type: CHART + CHART-CuCYDOlGEu: + children: [] + id: CHART-CuCYDOlGEu + meta: + chartId: 14 + height: 42 + sliceName: Table + sliceNameOverride: Raw data + uuid: 38d353dc-c0ef-41a0-97c7-3dcbebab9e02 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + - ROW-EaYAQjv4W + type: CHART + CHART-DxY0c3RnIv: + children: [] + id: CHART-DxY0c3RnIv + meta: + chartId: 3 + height: 61 + sliceName: Node counts + sliceNameOverride: General node visit distribution + uuid: 0c47c7b5-f500-46cb-97e3-9ebb637f0c8a + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-WirNNBBom + type: CHART + CHART-cxqeW2rGoV: + children: [] + id: CHART-cxqeW2rGoV + meta: + chartId: 7 + height: 50 + sliceName: Node Visits + sliceNameOverride: Node per dialog turn distribution + uuid: 44f4ab9d-5072-4926-a6ed-8615fb81b3d0 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-hNOZdWZpw + type: CHART + CHART-eZttETxtot: + children: [] + id: CHART-eZttETxtot + meta: + chartId: 16 + height: 50 + sliceName: Responses + uuid: 7844eb6d-4fce-44cf-8994-fb61a221a2ab + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-lvxd9vvnS5 + type: CHART + CHART-explore-10-1: + children: [] + id: CHART-explore-10-1 + meta: + chartId: 10 + height: 73 + sliceName: Transition layout + sliceNameOverride: Weighted transition graph + uuid: 801e8c66-f693-46e3-a9a5-7b2b0b08a4a7 + width: 6 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + - ROW-Ccs1FbcI- + type: CHART + CHART-explore-13-1: + children: [] + id: CHART-explore-13-1 + meta: + chartId: 11 + height: 74 + sliceName: Transition ratio [chord] + sliceNameOverride: Weighted transition graph [chord plot] + uuid: 388d2359-8d13-4795-8dc9-1cb5dfa92ee1 + width: 6 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + - ROW-Ccs1FbcI- + type: CHART + CHART-explore-14-1: + children: [] + id: CHART-explore-14-1 + meta: + chartId: 5 + height: 105 + sliceName: Node visits [sunburst] + sliceNameOverride: Node visit distribution [sunburst] + uuid: e955f48c-824c-423c-a11c-5b5dca162927 + width: 6 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-uSb7snkjI + type: CHART + CHART-explore-15-1: + children: [] + id: CHART-explore-15-1 + meta: + chartId: 2 + height: 50 + sliceName: Current topic [time series bar chart] + sliceNameOverride: Current topic slot [time series bar chart] + uuid: f8215b4d-cdaf-489a-90b2-040da840ab35 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-s_aeCbvt3 + type: CHART + CHART-explore-16-1: + children: [] + id: CHART-explore-16-1 + meta: + chartId: 4 + height: 50 + sliceName: Current topic slot [bar chart] + uuid: a70c05d0-770b-4068-a55d-934283f5b1bb + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-IsV-8cS3Z + type: CHART + CHART-explore-17-1: + children: [] + id: CHART-explore-17-1 + meta: + chartId: 1 + height: 50 + sliceName: Rating slot [line chart] + uuid: 128e1da2-1efd-433d-9b93-a07de175e2bc + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-TZwRV0tZy + type: CHART + CHART-explore-21-1: + children: [] + id: CHART-explore-21-1 + meta: + chartId: 13 + height: 60 + sliceName: Flow visit ratio monitor + uuid: ba02528b-184b-4304-b027-f2b7d9011ab0 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-ZTVWOu2o0 + type: CHART + CHART-explore-22-1: + children: [] + id: CHART-explore-22-1 + meta: + chartId: 15 + height: 61 + sliceName: Terminal labels + sliceNameOverride: Terminal labels monitor + uuid: cf066e41-a9e8-4f54-a875-ebf4da350b59 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-LhKWfqM7V + type: CHART + CHART-explore-9-1: + children: [] + id: CHART-explore-9-1 + meta: + chartId: 6 + height: 105 + sliceName: Node visits [ratio] + sliceNameOverride: Node visit distribution [ratio] + uuid: f9fb7893-3533-4519-bbc4-1f4853f380e1 + width: 6 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-uSb7snkjI + type: CHART + CHART-mYC2udeF3a: + children: [] + id: CHART-mYC2udeF3a + meta: + chartId: 9 + height: 50 + sliceName: Service load [users] + uuid: b5d43314-514c-464e-9fcc-f897e3ae0963 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-Ae7v1prsp2 + type: CHART + CHART-wci7CHiza6: + children: [] + id: CHART-wci7CHiza6 + meta: + chartId: 12 + height: 66 + sliceName: Transition counts + sliceNameOverride: General transitions distribution + uuid: 9fcc7cc1-9257-4c0d-b377-b3a60a8bf3df + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-UiCWq3ix1 + type: CHART + DASHBOARD_VERSION_KEY: v2 + GRID_ID: + children: + - TABS-Xoi5oUBxZI + id: GRID_ID + parents: + - ROOT_ID + type: GRID + HEADER_ID: + id: HEADER_ID + meta: + text: DFF statistics dashboard + type: HEADER + MARKDOWN-8Q9BhcEwva: + children: [] + id: MARKDOWN-8Q9BhcEwva + meta: + code: '## Current topic + + + Assuming that we extract the current topic as a slot on each dialog turn, + we can build a bar chart to see how various dialog topics are distributed. + The first of the two charts below aggregates the counts over dialog turns, + while the second one displays the distribution over time.' + height: 20 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-K0fThyfqJq + type: MARKDOWN + MARKDOWN-HncT4Y_WmV: + children: [] + id: MARKDOWN-HncT4Y_WmV + meta: + code: '## Flow visit ratio monitor + + + This chart aggregates the ratio of visits for each flow over the whole period + of service uptime. The aggregation period can be set using the filters on the left.' + height: 17 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-klrE6Idjlp + type: MARKDOWN + MARKDOWN-LyRuvHp-7m: + children: [] + id: MARKDOWN-LyRuvHp-7m + meta: + code: '## Weighted transition graph + + + The two weighted transition graphs emphasize the most frequently visited nodes + and transitions, effectively demonstrating the most popular routes inside + the dialog graph. + + + This can reveal characteristic features of user behavior given the current + dialog structure. + + +
+ + ' + height: 24 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + - ROW-FMNDn-yXPC + type: MARKDOWN + MARKDOWN-NXluIKVHl5: + children: [] + id: MARKDOWN-NXluIKVHl5 + meta: + code: '# Annotations + + + Annotations are values that your conversational service derives from user + requests and stores in the `Context` object. They are produced by `PROCESSING` + functions and by some supplemental services in the `Pipeline`. + + + To make them available in the dashboard, you need to define a custom extractor + function for them (see the [Extractor functions](https://deeppavlov.github.io/dialog_flow_framework/tutorials/tutorials.stats.1_extractor_functions.html) ). + The output of that function will then be persisted to the `data` column of + the logs table, while the name of the function will be available in the `data + key` column. That makes it easy to filter the relevant log entries and use + them to build a Superset chart. + + + If you need custom charts, consult the official Superset documentation for + instructions on how to create those: [link](https://superset.apache.org/docs/creating-charts-dashboards/exploring-data/#pivot-table). + + + It''s important to keep in mind that `data` is a JSON column, which is why + you need to specify your own `custom sql` expression to get the relevant value. + + + ```sql + + JSON_VALUE(data, ''$.key.nested_key'') + + ``` + + + Additionally, you may need to cast the extracted string to a numeric datatype + like below to use certain aggregation functions. + + ```sql + + AVG(CAST(JSON_VALUE(data, ''$.key'') AS Int16)) + + ``` + + + **Note that the charts below will only display meaningful figures if the respective + annotation values have been collected. To get those, run the sample data + provider script located in the `utils/stats/` folder.**' + height: 63 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-efGgRzySlP + type: MARKDOWN + MARKDOWN-Ne7griq2wJ: + children: [] + id: MARKDOWN-Ne7griq2wJ + meta: + code: '## Node per dialog turn distribution + + + This bar chart aggregates node labels over dialog turns. Not only does this + show the average dialog length, but it also allows you to observe + whether the path that users take on average deviates from the happy + path that you plan for the users. + +
+ + ' + height: 24 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-tzfO7Zd-dD + type: MARKDOWN + MARKDOWN-OCNK7Q0hyn: + children: [] + id: MARKDOWN-OCNK7Q0hyn + meta: + code: "## Rating slot\n\nLet's assume that you have a service that extracts\ + \ ratings from user requests as a numeric variable. Much like the previous\ + \ example, you can employ a variety of charts to analyze this data, such as\ + \ line charts or box plots. \n\nNote that to reproduce the chart below the\ + \ value will have to be cast to Int16 or to some other numeric datatype to\ + \ find the average.\n\n```sql\nAVG(CAST(JSON_VALUE(data, '$.key') AS Int16))\n\ + ```" + height: 33 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + - ROW-Hcn9FCMH6a + type: MARKDOWN + MARKDOWN-PY70BXaRGq: + children: [] + id: MARKDOWN-PY70BXaRGq + meta: + code: "# Overview\n\nThe `overview` section is a generalized representation\ + \ of the data related to your service. It allows you to inspect raw data entries\ + \ or to observe some general trends using weighted charts based on the dialog\ + \ graph.\n\n## Raw data\n\nRaw data shows unaggregated data points as a table.\ + \ \nThe `data` column offers a closer view of the collected keys and labels\ + \ in the form of JSON objects.\n\n
\n" + height: 33 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + - ROW-R1SOLtdPpz + type: MARKDOWN + MARKDOWN-XuW-kEED70: + children: [] + id: MARKDOWN-XuW-kEED70 + meta: + code: '# Node statistics + + + The `Node statistics` section mostly contains data related to how frequently + individual nodes are visited with the aim of providing you insights on how + well distinct parts of the graph function. + + + ## Node visits distribution + + + The charts below display the number of times that the flows and nodes of the + dialog graph were visited during graph traversal. These statistics are supposed + to help the developers better understand the importance of any particular + node inside the graph.' + height: 35 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-t7PnH6oKHU + type: MARKDOWN + MARKDOWN-aXRTn3gE5x: + children: [] + id: MARKDOWN-aXRTn3gE5x + meta: + code: '## Transitions distribution + + + The chart below shows how frequently each of the graph edges is traversed + by various users. + Based on this information, you can determine which transitions inside the + dialog graph function as intended. + + You can use the `Node` filter on the left + to select a slice of transitions that target a specific node. + +
' + height: 24 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-y6AC1wLv1j + type: MARKDOWN + MARKDOWN-i9daMe46XY: + children: [] + id: MARKDOWN-i9daMe46XY + meta: + code: '## Requests and responses + + + The table charts below make it possible to explore the user requests and bot + responses produced in the scope of your conversational service. You can also + use the `Dialog turn` and `User ID` filters on the left to only consider the + requests produced by a particular user or requests sent on a specific dialog turn. + + + Functions `get_last_request` and `get_last_response` need to be used to collect + the data.' + height: 26 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + - ROW-DKYV2zUmwU + type: MARKDOWN + MARKDOWN-pmSRDxroDW: + children: [] + id: MARKDOWN-pmSRDxroDW + meta: + code: '## Terminal labels monitor + + + The following chart dynamically measures which nodes appear the most as a + finishing point in the dialog, e.g. what are the dialog turns, after which + the users quit the conversation. This chart is very useful, since it allows + you to trace potentially unwanted behavior and see if any of the bot messages + is discouraging users from further interaction.' + height: 21 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-j6eWWjqran + type: MARKDOWN + MARKDOWN-w66E2L3xdo: + children: [] + id: MARKDOWN-w66E2L3xdo + meta: + code: '## Node visit ratio monitor + + + Like the `Flow visit ratio monitor`, the node visit ratio monitor aggregates + node visits over time, showing the proportion of unique users visiting each + node of the graph at a given moment in time. For instance, this can demonstrate + how the dynamics of user load change after some changes are made to the graph, + e.g. nodes are added or deleted. The length of time spans can be regulated + using filters.' + height: 26 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-CBSQJNAfEV + type: MARKDOWN + MARKDOWN-zISK-3R8Ta: + children: [] + id: MARKDOWN-zISK-3R8Ta + meta: + code: '# Service statistics + + + This section contains charts that aggregate statistics over time, covering + the whole uptime of the service. They can be of big help, when the task is + to trace the changes in user behavior when new features or fixes are being + introduced. + + + ## Service users + + + This plot aggregates the count of unique users querying the DFF service at + any given point in time. The time periods to aggregate over can be changed + using the filter on the left. + + ' + height: 35 + width: 12 + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + - ROW-zC6DVrW4wu + type: MARKDOWN + ROOT_ID: + children: + - GRID_ID + id: ROOT_ID + type: ROOT + ROW-7g6_n72hZ: + children: + - CHART-Af3zvLsiKV + id: ROW-7g6_n72hZ + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-Ae7v1prsp2: + children: + - CHART-mYC2udeF3a + id: ROW-Ae7v1prsp2 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-CBSQJNAfEV: + children: + - MARKDOWN-w66E2L3xdo + id: ROW-CBSQJNAfEV + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-Ccs1FbcI-: + children: + - CHART-explore-10-1 + - CHART-explore-13-1 + id: ROW-Ccs1FbcI- + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + type: ROW + ROW-DKYV2zUmwU: + children: + - MARKDOWN-i9daMe46XY + id: ROW-DKYV2zUmwU + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-EaYAQjv4W: + children: + - CHART-CuCYDOlGEu + id: ROW-EaYAQjv4W + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + type: ROW + ROW-FMNDn-yXPC: + children: + - MARKDOWN-LyRuvHp-7m + id: ROW-FMNDn-yXPC + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + type: ROW + ROW-Hcn9FCMH6a: + children: + - MARKDOWN-OCNK7Q0hyn + id: ROW-Hcn9FCMH6a + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-IsV-8cS3Z: + children: + - CHART-explore-16-1 + id: ROW-IsV-8cS3Z + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-K0fThyfqJq: + children: + - MARKDOWN-8Q9BhcEwva + id: ROW-K0fThyfqJq + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-LhKWfqM7V: + children: + - CHART-explore-22-1 + id: ROW-LhKWfqM7V + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-R1SOLtdPpz: + children: + - MARKDOWN-PY70BXaRGq + id: ROW-R1SOLtdPpz + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-JCU6rANFP + type: ROW + ROW-TZwRV0tZy: + children: + - CHART-explore-17-1 + id: ROW-TZwRV0tZy + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-UiCWq3ix1: + children: + - CHART-wci7CHiza6 + id: ROW-UiCWq3ix1 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-WirNNBBom: + children: + - CHART-DxY0c3RnIv + id: ROW-WirNNBBom + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-ZTVWOu2o0: + children: + - CHART-explore-21-1 + id: ROW-ZTVWOu2o0 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-efGgRzySlP: + children: + - MARKDOWN-NXluIKVHl5 + id: ROW-efGgRzySlP + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-hNOZdWZpw: + children: + - CHART-cxqeW2rGoV + id: ROW-hNOZdWZpw + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-j6eWWjqran: + children: + - MARKDOWN-pmSRDxroDW + id: ROW-j6eWWjqran + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-klrE6Idjlp: + children: + - MARKDOWN-HncT4Y_WmV + id: ROW-klrE6Idjlp + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + ROW-lvxd9vvnS5: + children: + - CHART-eZttETxtot + id: ROW-lvxd9vvnS5 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-oXaTD35jB: + children: + - CHART-91whs_IaiF + id: ROW-oXaTD35jB + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-s_aeCbvt3: + children: + - CHART-explore-15-1 + id: ROW-s_aeCbvt3 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-q4CdGUDlq + type: ROW + ROW-t7PnH6oKHU: + children: + - MARKDOWN-XuW-kEED70 + id: ROW-t7PnH6oKHU + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-tzfO7Zd-dD: + children: + - MARKDOWN-Ne7griq2wJ + id: ROW-tzfO7Zd-dD + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-uSb7snkjI: + children: + - CHART-explore-9-1 + - CHART-explore-14-1 + id: ROW-uSb7snkjI + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-y6AC1wLv1j: + children: + - MARKDOWN-aXRTn3gE5x + id: ROW-y6AC1wLv1j + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-Gw0Ffh1lG + type: ROW + ROW-zC6DVrW4wu: + children: + - MARKDOWN-zISK-3R8Ta + id: ROW-zC6DVrW4wu + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + - TAB-6zE8noCIsx + type: ROW + TAB-6zE8noCIsx: + children: + - ROW-zC6DVrW4wu + - ROW-Ae7v1prsp2 + - ROW-klrE6Idjlp + - ROW-ZTVWOu2o0 + - ROW-CBSQJNAfEV + - ROW-7g6_n72hZ + - ROW-j6eWWjqran + - ROW-LhKWfqM7V + id: TAB-6zE8noCIsx + meta: + defaultText: Tab title + placeholder: Tab title + text: Service statistics + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + type: TAB + TAB-Gw0Ffh1lG: + children: + - ROW-t7PnH6oKHU + - ROW-uSb7snkjI + - ROW-WirNNBBom + - ROW-y6AC1wLv1j + - ROW-UiCWq3ix1 + - ROW-tzfO7Zd-dD + - ROW-hNOZdWZpw + - ROW-DKYV2zUmwU + - ROW-oXaTD35jB + - ROW-lvxd9vvnS5 + id: TAB-Gw0Ffh1lG + meta: + defaultText: Tab title + placeholder: Tab title + text: Node statistics + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + type: TAB + TAB-JCU6rANFP: + children: + - ROW-R1SOLtdPpz + - ROW-EaYAQjv4W + - ROW-FMNDn-yXPC + - ROW-Ccs1FbcI- + id: TAB-JCU6rANFP + meta: + defaultText: Tab title + placeholder: Tab title + text: Overview + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + type: TAB + TAB-q4CdGUDlq: + children: + - ROW-efGgRzySlP + - ROW-K0fThyfqJq + - ROW-IsV-8cS3Z + - ROW-s_aeCbvt3 + - ROW-Hcn9FCMH6a + - ROW-TZwRV0tZy + id: TAB-q4CdGUDlq + meta: + defaultText: Tab title + placeholder: Tab title + text: Annotations + parents: + - ROOT_ID + - GRID_ID + - TABS-Xoi5oUBxZI + type: TAB + TABS-Xoi5oUBxZI: + children: + - TAB-JCU6rANFP + - TAB-Gw0Ffh1lG + - TAB-6zE8noCIsx + - TAB-q4CdGUDlq + id: TABS-Xoi5oUBxZI + meta: {} + parents: + - ROOT_ID + - GRID_ID + type: TABS +metadata: + color_scheme: echarts4Colors + label_colors: {} + shared_label_colors: + '0': '#c23531' + '1': '#2f4554' + '2': '#546570' + '3': '#c4ccd3' + '4': '#c23531' + '5': '#2f4554' + '6': '#61a0a8' + '7': '#d48265' + '8': '#91c7ae' + '9': '#749f83' + '10': '#61a0a8' + '11': '#d48265' + '12': '#91c7ae' + timed_refresh_immune_slices: [] + expanded_slices: {} + refresh_frequency: 1800 + show_native_filters: true + default_filters: '{}' + chart_configuration: {} + color_scheme_domain: + - '#c23531' + - '#2f4554' + - '#61a0a8' + - '#d48265' + - '#91c7ae' + - '#749f83' + - '#ca8622' + - '#bda29a' + - '#6e7074' + - '#546570' + - '#c4ccd3' + cross_filters_enabled: false + native_filter_configuration: + - id: NATIVE_FILTER-5vyW3SgU5 + controlValues: + enableEmptyFilter: false + name: Time grain + filterType: filter_timegrain + targets: + - datasetUuid: fda98ab8-f550-45f1-9ded-0113f3e67260 + defaultDataMask: + extraFormData: + time_grain_sqla: PT1M + filterState: + label: Minute + value: + - PT1M + cascadeParentIds: [] + scope: + rootPath: + - TAB-6zE8noCIsx + excluded: + - 5 + type: NATIVE_FILTER + description: '' + chartsInScope: + - 2 + - 4 + - 7 + - 13 + tabsInScope: + - TAB-6zE8noCIsx + - id: NATIVE_FILTER-k81Ybg31L + controlValues: + enableEmptyFilter: false + name: Time range + filterType: filter_time + targets: + - {} + defaultDataMask: + extraFormData: + time_range: 'DATEADD(DATETIME("now"), -1, day) : now' + filterState: + value: 'DATEADD(DATETIME("now"), -1, day) : now' + cascadeParentIds: [] + scope: + rootPath: + - TAB-6zE8noCIsx + excluded: [] + type: NATIVE_FILTER + description: '' + chartsInScope: + - 1 + - 2 + - 3 + - 4 + - 6 + - 8 + - 9 + - 10 + - 12 + - 13 + - 14 + - 15 + tabsInScope: + - TAB-6zE8noCIsx + - TAB-Gw0Ffh1lG + - TAB-JCU6rANFP + - id: NATIVE_FILTER-Q_v9J5gqV + controlValues: + enableEmptyFilter: false + name: History + filterType: filter_range + targets: + - column: + name: request_id + datasetUuid: fda98ab8-f550-45f1-9ded-0113f3e67260 + defaultDataMask: + extraFormData: {} + filterState: {} + ownState: {} + cascadeParentIds: [] + scope: + rootPath: + - TAB-Gw0Ffh1lG + excluded: [] + type: NATIVE_FILTER + description: '' + chartsInScope: + - 1 + - 3 + - 6 + - 9 + - 10 + tabsInScope: + - TAB-Gw0Ffh1lG + - id: NATIVE_FILTER-EGnz5obeE + controlValues: + enableEmptyFilter: false + defaultToFirstItem: false + multiSelect: true + searchAllOptions: false + inverseSelection: false + name: User id + filterType: filter_select + targets: + - column: + name: context_id + datasetUuid: fda98ab8-f550-45f1-9ded-0113f3e67260 + defaultDataMask: + extraFormData: {} + filterState: {} + ownState: {} + cascadeParentIds: [] + scope: + rootPath: + - TAB-Gw0Ffh1lG + - TAB-q4CdGUDlq + - TAB-JCU6rANFP + excluded: [] + type: NATIVE_FILTER + description: '' + chartsInScope: + - 1 + - 3 + - 6 + - 8 + - 9 + - 10 + - 12 + - 14 + - 15 + - 16 + - 17 + tabsInScope: + - TAB-Gw0Ffh1lG + - TAB-JCU6rANFP + - TAB-q4CdGUDlq + - id: NATIVE_FILTER-BU7iNmYAX + adhoc_filters: + - expressionType: SQL + sqlExpression: label <> '' + clause: WHERE + subject: null + operator: null + comparator: null + isExtra: false + isNew: false + datasourceWarning: false + filterOptionName: filter_68jo660l3a3_xn0lv9ghc2i + time_range: No filter + controlValues: + enableEmptyFilter: false + defaultToFirstItem: false + multiSelect: true + searchAllOptions: false + inverseSelection: false + name: Node + filterType: filter_select + targets: + - column: + name: label + datasetUuid: fda98ab8-f550-45f1-9ded-0113f3e67260 + defaultDataMask: + extraFormData: {} + filterState: {} + ownState: {} + cascadeParentIds: [] + scope: + rootPath: + - TAB-Gw0Ffh1lG + - TAB-q4CdGUDlq + excluded: [] + type: NATIVE_FILTER + description: '' + chartsInScope: + - 1 + - 3 + - 6 + - 9 + - 10 + - 15 + - 16 + - 17 + tabsInScope: + - TAB-Gw0Ffh1lG + - TAB-q4CdGUDlq + stagger_refresh: true +version: 1.0.0 diff --git a/dff/config/superset_dashboard/datasets/dff_database/dff_final_nodes.yaml b/dff/config/superset_dashboard/datasets/dff_database/dff_final_nodes.yaml index a07fdd800..4a8628298 100644 --- a/dff/config/superset_dashboard/datasets/dff_database/dff_final_nodes.yaml +++ b/dff/config/superset_dashboard/datasets/dff_database/dff_final_nodes.yaml @@ -5,14 +5,7 @@ default_endpoint: null offset: 0 cache_timeout: null schema: test -sql: "\nWITH main AS (\n SELECT LogAttributes['context_id'] AS context_id,\n \ - \ max(LogAttributes['request_id']) AS max_history\n FROM otel_logs\nGROUP BY\ - \ context_id\n)\nSELECT DISTINCT LogAttributes['context_id'] AS context_id,\nLogAttributes['request_id']\ - \ AS request_id,\notel_logs.Timestamp AS start_time,\nJSON_VALUE(otel_logs.Body,\ - \ '$.label') AS label,\nJSON_VALUE(otel_logs.Body, '$.flow') AS flow_label,\nJSON_VALUE(otel_logs.Body,\ - \ '$.node') AS node_label\nFROM otel_logs\nINNER JOIN main\nON context_id = main.context_id\n\ - AND request_id = main.max_history\nINNER JOIN otel_traces\nON otel_logs.TraceId\ - \ = otel_traces.TraceId\nWHERE otel_traces.SpanName = 'get_current_label'\n" +sql: null params: null template_params: null filter_select_enabled: false @@ -33,7 +26,7 @@ columns: verbose_name: null is_dttm: true is_active: true - type: DateTime64(9) + type: DateTime advanced_data_type: null groupby: true filterable: true @@ -81,7 +74,7 @@ columns: verbose_name: null is_dttm: false is_active: true - type: String + type: Nullable(UInt64) advanced_data_type: null groupby: true filterable: true diff --git a/dff/config/superset_dashboard/datasets/dff_database/dff_node_stats.yaml b/dff/config/superset_dashboard/datasets/dff_database/dff_node_stats.yaml index 5ddab7c69..4e2cc5792 100644 --- a/dff/config/superset_dashboard/datasets/dff_database/dff_node_stats.yaml +++ b/dff/config/superset_dashboard/datasets/dff_database/dff_node_stats.yaml @@ -5,17 +5,7 @@ default_endpoint: null offset: 0 cache_timeout: null schema: test -sql: "\nWITH main AS (\n SELECT DISTINCT otel_logs.LogAttributes['context_id']\ - \ as context_id,\n otel_logs.LogAttributes['request_id'] as request_id,\n \ - \ otel_traces.Timestamp as start_time,\n otel_traces.SpanName as data_key,\n\ - \ otel_logs.Body as data,\n JSON_VALUE(otel_logs.Body, '$.label') as label,\n\ - \ JSON_VALUE(otel_logs.Body, '$.flow') as flow_label,\n JSON_VALUE(otel_logs.Body,\ - \ '$.node') as node_label,\n otel_logs.TraceId as trace_id,\n otel_traces.TraceId\n\ - FROM otel_logs, otel_traces\n WHERE otel_logs.TraceId = otel_traces.TraceId and\ - \ otel_traces.SpanName = 'get_current_label'\n ORDER BY context_id, request_id\n\ - ) SELECT context_id,\n request_id,\n start_time,\n data_key,\n data,\n\ - \ label,\n neighbor(label, -1) as prev_label,\n flow_label,\n node_label\n\ - FROM main\nWHERE label != ''\n" +sql: null params: null template_params: null filter_select_enabled: false @@ -80,6 +70,18 @@ columns: description: null python_date_format: null extra: {} +- column_name: prev_flow + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} - column_name: flow_label verbose_name: null is_dttm: false @@ -108,7 +110,7 @@ columns: verbose_name: null is_dttm: false is_active: true - type: String + type: Nullable(UInt64) advanced_data_type: null groupby: true filterable: true diff --git a/dff/config/superset_dashboard/datasets/dff_database/dff_stats.yaml b/dff/config/superset_dashboard/datasets/dff_database/dff_stats.yaml new file mode 100644 index 000000000..6f1efe683 --- /dev/null +++ b/dff/config/superset_dashboard/datasets/dff_database/dff_stats.yaml @@ -0,0 +1,146 @@ +table_name: dff_stats +main_dttm_col: null +description: null +default_endpoint: null +offset: 0 +cache_timeout: null +schema: test +sql: null +params: null +template_params: null +filter_select_enabled: false +fetch_values_predicate: null +extra: null +uuid: 8ba2e188-2bf8-4809-a5ee-2477a539d493 +metrics: +- metric_name: count + verbose_name: null + metric_type: null + expression: count(*) + description: null + d3format: null + extra: {} + warning_text: null +columns: +- column_name: data_key + verbose_name: null + is_dttm: false + is_active: true + type: LowCardinality(String) + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: start_time + verbose_name: null + is_dttm: true + is_active: true + type: DateTime + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: node_label + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: prev_label + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: flow_label + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: context_id + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: request_id + verbose_name: null + is_dttm: false + is_active: true + type: Nullable(UInt64) + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: prev_flow + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: data + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +- column_name: label + verbose_name: null + is_dttm: false + is_active: true + type: String + advanced_data_type: null + groupby: true + filterable: true + expression: null + description: null + python_date_format: null + extra: {} +version: 1.0.0 +database_uuid: ee32f76e-55e3-483a-935a-22d03072db23 diff --git a/dff/config/superset_dashboard/metadata.yaml b/dff/config/superset_dashboard/metadata.yaml index b68dadba1..421997817 100644 --- a/dff/config/superset_dashboard/metadata.yaml +++ b/dff/config/superset_dashboard/metadata.yaml @@ -1,3 +1,3 @@ version: 1.0.0 type: Dashboard -timestamp: '2023-07-25T14:27:32.324677+00:00' +timestamp: '2023-09-15T11:46:43.125179+00:00' diff --git a/dff/stats/__main__.py b/dff/stats/__main__.py index 396273ea2..92ca42e87 100644 --- a/dff/stats/__main__.py +++ b/dff/stats/__main__.py @@ -73,6 +73,7 @@ def main(parsed_args: Optional[argparse.Namespace] = None): parser.add_argument("-dn", "--db.name", help="Name of the database.") parser.add_argument("-dt", "--db.table", default="otel_logs", help="Name of the table.") parser.add_argument("-o", "--outfile", help="Optionally persist the configuration as a zip file.") + parser.add_argument("-i", "--infile", help="Configuration zip file to import.") parser.add_argument("-H", "--host", default="localhost", help="Superset host") parser.add_argument("-p", "--port", default="8088", help="Superset port.") parser.add_argument("-U", "--username", required=True, help="Superset user.") @@ -100,11 +101,18 @@ def main(parsed_args: Optional[argparse.Namespace] = None): if parsed_args is None: parsed_args = parser.parse_args(sys.argv[1:]) - outfile = make_zip_config(parsed_args) - import_dashboard(parsed_args, zip_file=str(outfile)) + file = None + use_infile = hasattr(parsed_args, "infile") and parsed_args.infile is not None + use_outfile = hasattr(parsed_args, "outfile") and parsed_args.outfile is not None + if not use_infile: + file = make_zip_config(parsed_args) + else: + file = parsed_args.infile - if not hasattr(parsed_args, "outfile") or parsed_args.outfile is None: - outfile.unlink() + import_dashboard(parsed_args, zip_file=str(file)) + + if not use_infile and not use_outfile: + file.unlink() if __name__ == "__main__": diff --git a/dff/stats/cli.py b/dff/stats/cli.py index 20058a990..2b881b00d 100644 --- a/dff/stats/cli.py +++ b/dff/stats/cli.py @@ -4,6 +4,7 @@ This modules defines commands that can be called via the command line interface. """ +from uuid import uuid4 import tempfile import shutil import sys @@ -60,7 +61,7 @@ DFF_NODE_STATS_STATEMENT = """ WITH main AS ( SELECT DISTINCT {table}.LogAttributes['context_id'] as context_id, - {table}.LogAttributes['request_id'] as request_id, + toUInt64OrNull({table}.LogAttributes['request_id']) as request_id, toDateTime(otel_traces.Timestamp) as start_time, otel_traces.SpanName as data_key, {table}.Body as data, @@ -69,7 +70,7 @@ {nodefield} as node_label, {table}.TraceId as trace_id, otel_traces.TraceId\nFROM {table}, otel_traces - WHERE {table}.TraceId = otel_traces.TraceId and otel_traces.SpanName = 'get_current_label' + WHERE {table}.TraceId = otel_traces.TraceId and data_key = 'get_current_label' ORDER BY context_id, request_id ) SELECT context_id, request_id, @@ -77,44 +78,47 @@ data_key, data, label, - {lag} as prev_label, + {label_lag} as prev_label, + {flow_lag} as prev_flow, flow_label, node_label FROM main -WHERE label != '' """ -DFF_ACYCLIC_NODES_STATEMENT = """ +DFF_STATS_STATEMENT = """ WITH main AS ( SELECT DISTINCT {table}.LogAttributes['context_id'] as context_id, - {table}.LogAttributes['request_id'] as request_id, - {table}.Timestamp as timestamp, - {lblfield} as label\nFROM {table} - INNER JOIN -( - WITH helper AS ( - SELECT DISTINCT {table}.LogAttributes['context_id'] as context_id, - {table}.LogAttributes['request_id'] as request_id, - {lblfield} as label - FROM {table} - ) - SELECT context_id FROM helper - GROUP BY context_id - HAVING COUNT(context_id) = COUNT(DISTINCT label) -) as plain_ctx -ON plain_ctx.context_id = context_id -ORDER by context_id, request_id -) -SELECT * FROM main + toUInt64OrNull({table}.LogAttributes['request_id']) as request_id, + toDateTime(otel_traces.Timestamp) as start_time, + otel_traces.SpanName as data_key, + {table}.Body as data, + {lblfield} as label, + {flowfield} as flow_label, + {nodefield} as node_label, + {table}.TraceId as trace_id, + otel_traces.TraceId\nFROM {table}, otel_traces + WHERE {table}.TraceId = otel_traces.TraceId + ORDER BY data_key, context_id, request_id +) SELECT context_id, + request_id, + start_time, + data_key, + data, + label, + {label_lag} as prev_label, + {flow_lag} as prev_flow, + flow_label, + node_label +FROM main """ DFF_FINAL_NODES_STATEMENT = """ WITH main AS ( SELECT LogAttributes['context_id'] AS context_id, - max(LogAttributes['request_id']) AS max_history + max(toUInt64OrNull(LogAttributes['request_id'])) AS max_history FROM {table}\nGROUP BY context_id ) SELECT DISTINCT LogAttributes['context_id'] AS context_id, -LogAttributes['request_id'] AS request_id, -{table}.Timestamp AS start_time, +toUInt64OrNull({table}.LogAttributes['request_id']) AS request_id, +toDateTime(otel_traces.Timestamp) AS start_time, {lblfield} AS label, {flowfield} AS flow_label, {nodefield} AS node_label @@ -128,7 +132,7 @@ """ SQL_STATEMENT_MAPPING = { - "dff_acyclic_nodes.yaml": DFF_ACYCLIC_NODES_STATEMENT, + "dff_stats.yaml": DFF_STATS_STATEMENT, "dff_node_stats.yaml": DFF_NODE_STATS_STATEMENT, "dff_final_nodes.yaml": DFF_FINAL_NODES_STATEMENT, } @@ -184,7 +188,7 @@ def make_zip_config(parsed_args: argparse.Namespace) -> Path: if hasattr(parsed_args, "outfile") and parsed_args.outfile: outfile_name = parsed_args.outfile else: - outfile_name = "temp.zip" + outfile_name = f"config_{str(uuid4())}.zip" file_conf = OmegaConf.load(parsed_args.file) sys.argv = [__file__] + [f"{key}={value}" for key, value in parsed_args.__dict__.items() if value] @@ -194,7 +198,12 @@ def make_zip_config(parsed_args: argparse.Namespace) -> Path: if OmegaConf.select(cli_conf, "db.driver") == "clickhousedb+connect": params = dict( table="${db.table}", - lag="neighbor(label, -1)", + label_lag="lagInFrame(label) OVER " + "(PARTITION BY context_id ORDER BY request_id ASC " + "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)", + flow_lag="lagInFrame(flow_label) OVER " + "(PARTITION BY context_id ORDER BY request_id ASC " + "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)", texttype="String", lblfield="JSON_VALUE(${db.table}.Body, '$.label')", flowfield="JSON_VALUE(${db.table}.Body, '$.flow')", diff --git a/dff/stats/default_extractors.py b/dff/stats/default_extractors.py index d48cd6d26..f90a3e3ad 100644 --- a/dff/stats/default_extractors.py +++ b/dff/stats/default_extractors.py @@ -13,18 +13,23 @@ from datetime import datetime from dff.script import Context -from dff.pipeline import ExtraHandlerRuntimeInfo +from dff.pipeline import ExtraHandlerRuntimeInfo, Pipeline from .utils import get_wrapper_field -async def get_current_label(ctx: Context, _, info: ExtraHandlerRuntimeInfo): +async def get_current_label(ctx: Context, pipeline: Pipeline, info: ExtraHandlerRuntimeInfo): """ Extract the current label on each turn. This function is required for running the dashboard with the default configuration. + + .. note:: + + Preferrably, it needs to be invoked as `after_handler` of the `Actor` service. + """ last_label = ctx.last_label if last_label is None: - return {"flow": None, "node": None, "label": None} + last_label = pipeline.actor.start_label[:2] return {"flow": last_label[0], "node": last_label[1], "label": ": ".join(last_label)} @@ -53,3 +58,33 @@ async def get_timing_after(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # n start_time = ctx.framework_states[get_wrapper_field(info, "time")] data = {"execution_time": str(datetime.now() - start_time)} return data + + +async def get_last_response(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + """ + Extract the text of the last response in the current context. + This handler is best used together with the `ACTOR` component. + + This function is required to enable charts that aggregate requests and responses. + """ + data = {"last_response": ctx.last_response.text} + return data + + +async def get_last_request(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + """ + Extract the text of the last request in the current context. + This handler is best used together with the `ACTOR` component. + + This function is required to enable charts that aggregate requests and responses. + """ + data = {"last_request": ctx.last_request.text} + return data + + +__all__ = ["get_current_label", "get_timing_before", "get_timing_after", "get_last_request", "get_last_response"] +""" +List of exported functions. + +:meta hide-avlue: +""" diff --git a/dff/stats/instrumentor.py b/dff/stats/instrumentor.py index a053c532b..41f7c2ca5 100644 --- a/dff/stats/instrumentor.py +++ b/dff/stats/instrumentor.py @@ -62,7 +62,7 @@ class OtelInstrumentor(BaseInstrumentor): .. code-block:: @dff_instrumentor - def function(context, pipeline, runtime_info): + async def function(context, pipeline, runtime_info): ... :param logger_provider: Opentelemetry logger provider. Used to construct a logger instance. @@ -126,14 +126,12 @@ def _instrument(self, logger_provider=None, tracer_provider=None, meter_provider self._configure_providers( logger_provider=logger_provider, tracer_provider=tracer_provider, meter_provider=meter_provider ) - wrap_function_wrapper(default_extractors, "get_current_label", self.__call__.__wrapped__) - wrap_function_wrapper(default_extractors, "get_timing_before", self.__call__.__wrapped__) - wrap_function_wrapper(default_extractors, "get_timing_after", self.__call__.__wrapped__) + for func_name in default_extractors.__all__: + wrap_function_wrapper(default_extractors, func_name, self.__call__.__wrapped__) def _uninstrument(self, **kwargs): - unwrap(default_extractors, "get_current_label") - unwrap(default_extractors, "get_timing_before") - unwrap(default_extractors, "get_timing_after") + for func_name in default_extractors.__all__: + unwrap(default_extractors, func_name) def _configure_providers(self, logger_provider, tracer_provider, meter_provider): self._logger_provider = logger_provider or get_logger_provider() diff --git a/dff/utils/docker/dockerfile_stats b/dff/utils/docker/dockerfile_stats index 5017fd975..9c40071ea 100644 --- a/dff/utils/docker/dockerfile_stats +++ b/dff/utils/docker/dockerfile_stats @@ -2,5 +2,7 @@ FROM apache/superset:2.1.0rc1 USER root RUN cd /app && pip install .[clickhouse] COPY entrypoint_stats.sh /app/docker/ +COPY --chown=superset superset_config_docker.py /app/pythonpath/ +ENV SUPERSET_CONFIG_PATH /app/pythonpath/superset_config_docker.py USER superset ENTRYPOINT ["/bin/bash","/app/docker/entrypoint_stats.sh"] \ No newline at end of file diff --git a/dff/utils/docker/entrypoint_stats.sh b/dff/utils/docker/entrypoint_stats.sh index 663a39b75..cac946f7d 100644 --- a/dff/utils/docker/entrypoint_stats.sh +++ b/dff/utils/docker/entrypoint_stats.sh @@ -2,6 +2,7 @@ export SERVER_THREADS_AMOUNT=8 set -m nohup /bin/bash /usr/bin/run-server.sh & +sleep 5 superset fab create-admin --firstname superset --lastname admin --username $SUPERSET_USERNAME --email admin@admin.com --password $SUPERSET_PASSWORD superset db upgrade superset init diff --git a/dff/utils/docker/superset_config_docker.py b/dff/utils/docker/superset_config_docker.py new file mode 100644 index 000000000..ca138927f --- /dev/null +++ b/dff/utils/docker/superset_config_docker.py @@ -0,0 +1,32 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# This is an example "local" configuration file. In order to set/override config +# options that ONLY apply to your local environment, simply copy/rename this file +# to docker/pythonpath/superset_config_docker.py +# It ends up being imported by docker/superset_config.py which is loaded by +# superset/config.py +# +import os + +SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{0}:{1}@dashboard-metadata:{2}/{3}".format( + os.getenv("POSTGRES_USERNAME"), + os.getenv("POSTGRES_PASSWORD"), + os.getenv("SUPERSET_METADATA_PORT"), + os.getenv("POSTGRES_DB"), +) diff --git a/dff/utils/testing/toy_script.py b/dff/utils/testing/toy_script.py index 78a3f0fb1..f035df047 100644 --- a/dff/utils/testing/toy_script.py +++ b/dff/utils/testing/toy_script.py @@ -62,3 +62,143 @@ :meta hide-value: """ + +MULTIFLOW_SCRIPT = { + "root": { + "start": { + RESPONSE: Message(text="Hi"), + TRANSITIONS: { + ("small_talk", "ask_some_questions"): exact_match(Message(text="hi")), + ("animals", "have_pets"): exact_match(Message(text="i like animals")), + ("animals", "like_animals"): exact_match(Message(text="let's talk about animals")), + ("news", "what_news"): exact_match(Message(text="let's talk about news")), + }, + }, + "fallback": {RESPONSE: Message(text="Oops")}, + }, + "animals": { + "have_pets": { + RESPONSE: Message(text="do you have pets?"), + TRANSITIONS: {"what_animal": exact_match(Message(text="yes"))}, + }, + "like_animals": { + RESPONSE: Message(text="do you like it?"), + TRANSITIONS: {"what_animal": exact_match(Message(text="yes"))}, + }, + "what_animal": { + RESPONSE: Message(text="what animals do you have?"), + TRANSITIONS: { + "ask_about_color": exact_match(Message(text="bird")), + "ask_about_breed": exact_match(Message(text="dog")), + }, + }, + "ask_about_color": {RESPONSE: Message(text="what color is it")}, + "ask_about_breed": { + RESPONSE: Message(text="what is this breed?"), + TRANSITIONS: { + "ask_about_breed": exact_match(Message(text="pereat")), + "tell_fact_about_breed": exact_match(Message(text="bulldog")), + "ask_about_training": exact_match(Message(text="I don't know")), + }, + }, + "tell_fact_about_breed": { + RESPONSE: Message(text="Bulldogs appeared in England as specialized bull-baiting dogs. "), + }, + "ask_about_training": {RESPONSE: Message(text="Do you train your dog? ")}, + }, + "news": { + "what_news": { + RESPONSE: Message(text="what kind of news do you prefer?"), + TRANSITIONS: { + "ask_about_science": exact_match(Message(text="science")), + "ask_about_sport": exact_match(Message(text="sport")), + }, + }, + "ask_about_science": { + RESPONSE: Message(text="i got news about science, do you want to hear?"), + TRANSITIONS: { + "science_news": exact_match(Message(text="yes")), + ("small_talk", "ask_some_questions"): exact_match(Message(text="let's change the topic")), + }, + }, + "science_news": { + RESPONSE: Message(text="This is science news"), + TRANSITIONS: { + "what_news": exact_match(Message(text="ok")), + ("small_talk", "ask_some_questions"): exact_match(Message(text="let's change the topic")), + }, + }, + "ask_about_sport": { + RESPONSE: Message(text="i got news about sport, do you want to hear?"), + TRANSITIONS: { + "sport_news": exact_match(Message(text="yes")), + ("small_talk", "ask_some_questions"): exact_match(Message(text="let's change the topic")), + }, + }, + "sport_news": { + RESPONSE: Message(text="This is sport news"), + TRANSITIONS: { + "what_news": exact_match(Message(text="ok")), + ("small_talk", "ask_some_questions"): exact_match(Message(text="let's change the topic")), + }, + }, + }, + "small_talk": { + "ask_some_questions": { + RESPONSE: Message(text="how are you"), + TRANSITIONS: { + "ask_talk_about": exact_match(Message(text="fine")), + ("animals", "like_animals"): exact_match(Message(text="let's talk about animals")), + ("news", "what_news"): exact_match(Message(text="let's talk about news")), + }, + }, + "ask_talk_about": { + RESPONSE: Message(text="what do you want to talk about"), + TRANSITIONS: { + ("animals", "like_animals"): exact_match(Message(text="dog")), + ("news", "what_news"): exact_match(Message(text="let's talk about news")), + }, + }, + }, +} +""" +Simple dialog with multiple flows. + +:meta hide-value: +""" + +MULTIFLOW_REQUEST_OPTIONS = { + "root": { + "start": [ + "hi", + "i like animals", + "let's talk about animals", + ] + }, + "animals": { + "have_pets": ["yes"], + "like_animals": ["yes"], + "what_animal": ["bird", "dog"], + "ask_about_breed": ["pereat", "bulldog", "I don't know"], + }, + "news": { + "what_news": ["science", "sport"], + "ask_about_science": ["yes", "let's change the topic"], + "science_news": ["ok", "let's change the topic"], + "ask_about_sport": ["yes", "let's change the topic"], + "sport_news": ["ok", "let's change the topic"], + }, + "small_talk": { + "ask_some_questions": [ + "fine", + "let's talk about animals", + "let's talk about news", + ], + "ask_talk_about": ["dog", "let's talk about news"], + }, +} +""" +Request options for automated client requests for :py:data:`~.MULTIFLOW_SCRIPT`. + +:meta hide-value: +""" diff --git a/docker-compose.yml b/docker-compose.yml index 382dc1dea..3c88bc854 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,10 +50,33 @@ services: context: ./dff/utils/docker dockerfile: dockerfile_stats image: ghcr.io/deeppavlov/superset_df_dashboard:latest + depends_on: + dashboard-metadata: + condition: service_healthy + clickhouse: + condition: service_started profiles: - stats ports: - "8088:8088" + dashboard-metadata: + env_file: [.env_file] + image: postgres:latest + profiles: + - stats + restart: unless-stopped + expose: + - "5433" + ports: + - 5433:5433 + command: -p 5433 + healthcheck: + test: pg_isready -p 5433 --username=$${POSTGRES_USERNAME} + interval: 4s + timeout: 3s + retries: 3 + volumes: + - dashboard-data:/var/lib/postgresql/data clickhouse: env_file: [.env_file] image: clickhouse/clickhouse-server:latest @@ -66,6 +89,11 @@ services: - '9000:9000' volumes: - ch-data:/var/lib/clickhouse/ + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 + interval: 5s + timeout: 4s + retries: 5 otelcol: image: otel/opentelemetry-collector-contrib:latest profiles: @@ -73,6 +101,9 @@ services: container_name: otel-col restart: unless-stopped command: [ "--config=/etc/otelcol-config.yml", "--config=/etc/otelcol-config-extras.yml" ] + depends_on: + clickhouse: + condition: service_healthy volumes: - ./dff/utils/otel/otelcol-config.yml:/etc/otelcol-config.yml:ro - ./dff/utils/otel/otelcol-config-extras.yml:/etc/otelcol-config-extras.yml:ro @@ -82,3 +113,5 @@ services: volumes: ch-data: name: "ch-data" + dashboard-data: + name: "dashboard-data" diff --git a/docs/source/_static/images/annotations.png b/docs/source/_static/images/annotations.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e524890a8345a7f15acf071f5634524c7b61f4 GIT binary patch literal 64834 zcmeFZg;Si*wk{ln1P$>!Xs?cXyXTf;$8oLV&>F?(R--ha}kG?iw5h9T;GMfseh< z`R#L0)mQf~xLq|hwce@juDAPH(!HKFATdiX57u@P4mfAOL&7WKjW?X&#eMNZ%S#S47ee|=ucYtWp( zc=50xFD0(^)#P{$-Ct`iTkJ75GSF4LL^=VVwnjmx-f}JjMUm!lbmHd@@)wjO-2QjC zZ%6lVVKgi>-ICa=Y>OajRU(kII?NMBm zp6Z`g!2jpB`9A_x0e|2Blm5LL|9>lynGqwP-c*M&4p4S`kM-Ye3cg=()_p%tox44) z$d){@tbnt6pIzqGwz+J|X2OUa@2^QbJv|ebF{y4WF{`tBMzD2SZdImfO8e3MTA`Cy zP(MKWpAGWp=6Byy5+WiMT?o&Zi&TRu2-p$ta^k#Su-PWqE&)KZhc?+cn!0_HVko}B z=b*&}rlRYH0_3FRbpO4D1()*ogruTILEpnr)Vnu65rm}1@sP2Jh!EKt@4m8o8C_;j z|3-TQoGK@^7cvAsW;I>*tU{qxtepI4x&>8IN>2d2fkPiA!=YhFg=P);RTx>3N2$>9 zhRcut%<#1o4Ws2GtM^`!Ko0Sf=GcJMIk72HeDq<#zbpM8UN zF6e7x0mZ2`A1krb(gh9P;W1<+M_!R*wEY<9>Op%uq4jJ1C#O1>advQlcZIs4VWjE; zo0N%jTh@P%7k(f-BNN=~q}9z&VTHV_nEd)sQ^#yZsMh?hOsNQ_^jX6D;vhR_^}V#- z#iJk#>5*7N<^7sPl0zcfKO=;PGgvrs?Vqn>uZk29s=zhkh3z=zh2znm?t@=vx#wH1 zy5OG0)xI5^487fKBi*=<5mf(MmC6>vJRRrmp5Tx5(ZyJniPKpTqo>+E+NC;>WH+~C zaxy7=R$P-^Kr!9uS9H6hwu_Imjn_w5vl2}tIxkt3)RGX73z^gB!P9hbW_F}jTZ($( zqRnoJ0)H&e5bb9RH@azORat|Z4`RIpE>$r}N|q+2+43`~JPd(7*q(O}zfxO%h#O$SsnillYO<5d;C-RK&h<1@ZGnQ1rZU9X5(vS3ZE_T4(+FXG)9p3u!Bbms zHLbitxDP!LY8vIsmeRdgx4T%!fHvLwMcj5Ql=YVdP#Jc=`_SstL zpXmsp%yTh%IgEBmQ##>`$xO#^Rv~$qMwxa-)~O=(8^b5ecQ*e5CKT3 z)e2))=XVxms(^ed8Fu*y8y^A(dI(RC(Y2xT>%h_mc=C@I-ZHEoa4wZBu7|hjJy>ES zTs3OG$!SPdJG4`SOQDo>F`9>HB&u6X)z!1~1{I(5kWkEzb5M%OMz zFnCweY$F@1Rb-MIVVNit16gO0k#F+PKUh>Z*7cz|)RZeDl{>2bq)U-aor#$buxSIH z(VXF8SJcRNJA%R%t8+65tP5Nk0*3{Bi>PH0Y99%f7VdMSI@$?maw5#~`~n$K2xiM5 zjE4yVS`L+_n{7-hGf1#PBK}$*b8PEwk~qlwAq!IjV9DEuR*SVg3EXbWEX&Y;3_{s% z)8poOE(w}@EmK1Ls-~#p8OB1P390k7WaLFo1AbG}@^;gd^$ZF_-(A?t!7(2O1LNp5 zH^lN99IW+pD$~yuTK2rZ^q3iuo`wtN7m04?_e1?hkVB#$FZ+Xw8T85Bz(h4S%zuy5 zqaTvzZnZiC*MuPFCNQVY8;=<0Wi)NFVX>-&iO?13oaaJU!v~+P9Zqc~DE^lfb&7^Y zu6OkADMo?fIZvb`UC^Iu8!2CRHt15BMcHI7G`8W4Vmhrz6b{Bgp-0HCr1c)gb*eJ6 zPrO-S_WOnWtDA^@75@GXw;UiQMWlz%i9lU-GUWXC0A8Ad5+MIH8awGEnT@Kch2~&! zhOYC+k>eRCz!u(S4H?wC33u+c3cVcT)RN-DR{qDA;TF$RkUqAPcM}=nVR&3oB-#&> zf^!hW_^LxPZuD((?>ZrKsuLG_t^TZT&MM!$c`lG(@F^6nPJ2QuT*!F9Z_ApMnDVrT z2csRrQ>SWuL-M$opac%!wMEpKO=oX2b%HF5)FT@rP;NXRkM(M8th&lAAbleiPY_8( zc1qF~7+gNwRusef=SqFRy&23*i@3`sbypOVRg8MPp&(y|+`3uPguVtcnq( z;|C8$uh^Nj+7KL4yTv=fdM%CJ<3zyh!oG{|ZOYoh{A{R&Bth(#Ef2wnXn<(ySvBJnU*(4aEs98W)6#=GDO-cwOdm=r)|t{-UZLdENkT- z8E-G=$B4XtIwP0~aJPw+bxS?l-S`Ka;RYrUOblCp42QW zz?EjEP9*vVP)1~X1-97>anSQ+K1q^6VzCj(EUGWC9#afQ?&~o;2jHpkG2D^jPIItB zxwB8Xm3C_TPkv#KoEq`?1use-X~x7HDvRC)1#tW*6>dqCqWxDRIv9BgGkaCdrcEzSM2(dhQnZU&-F)yqh?I`g@Dc}Hq z>jMA%w?&$=Pnei7JbB`xJOuvxK5nreZjfYs34ZEwCOiO(Sl_G+s{DhRS+C_ydW>^ zx2^EWgjXe={6?|uYBMoem6C{;aHC`t*)onx5mx?PO)jTYR32#CksZm5I}Nf@I`--R zc->_+5;Tn0@{;axGuqv1S!GfeSsJZ>*5(-JnT<~ZQ(dP|4&_oOl*!5lMI%piP8wYY z-B9H^eh0>N4iN2qb=ZLUlD7+@+XBVdbwL$3lA{o{6RG42+0FJ6N*EG+b@Y)~D}Xf@ z*|o4j@FS*^0(u&?P{?VyT(j%3r{W+LEp7ijVFx$szAoiIu_Ny^U2w0#HQPBvf4E1`c4<(+g&7vX|b@DTN+wPnd0t&vCQGqccE& zuR0)Rg$sc+1PL^ZNI*cP^W0T%Hww|ItkHBzX3|15Dm@KDP+_t=GkW}Ln3uDvl3rdJ z{x#(W#t{nJFMgh8q0%ZUBAOGqxp9^#1K*5;>NnB?Kl}yu5YnFAjS)v3HyS!T{LCGy z>ruA=9zVyU*D)95yEH}DDk&5H6U~^cpkbg6ZA0U8|B4;Yw>A+1xEk0kx0G;M872D% z+>%%sel>O=ZO;FG%dp{0+Uou>;E&}FERM~Sub6txIpaN_9ez~*`A|Q@%Cm#k|FKox z%kkBY=%`)L0qvpbg)D}fEFtn6do|%xCtRm0$wc$>`a?Z*g%TBMQ0I2jMILHRT^M9Rf*8eWwpvH4HDRqC4`6P3Kvjh!I7fgyF`VL=zIpVS7Y!q~{FqCRN z-lit#%Ce`6Oz!w9q4o*OWi+%sPd+h`qx(u^hXfhtgcOomOz5}o-@DrGqh};U5Mqls8|Eemt-05mWa!0;PWw&Aqr$qHQ&9xVlcGygV3$V)LH-4}1;0ZM7nnm~j`bp*EAxdWx+P%%?qlLAHVT&B> zN~Xc_2oFtLWjp^$qTaG#X{MJIxE{%jK>lGUchr>_zpx@4ZFls&^yr;3=}j#(&aRVs zu@cGirKTl6(s;l9#I8XV1q0Difl?dw*@~Pq-gtw;m&s6zv5%PS}%Z1eS{s?^m zIdnpI&nr3Io0ik}kl(vH3p_Y8sm}d=6RI-TF#O#a!qA5uFw~mL~t4oLvmpJf&XT zx0A5Q4IGFS^b(g?2oHOF#r-?r7`IzD$AqOh@xzIif$GeA*T=!`KiBO@dG?%6@+uOk zr!in;F~dJ?blkjI32dykj~7`^QiY<4GBohY(;jkJBi7km6{&g#0(oPc&3E1(%u80( zh7AyZ1HK$w)!y!(jxpL$mJOPO)q2h6zvPX{{s#nQp$sz$jU&I!3)1Si2)O=TJOApNn>rcp2-z8dHU{$N`vCuO z8l?(QPT;j|f<^p^#L5%HJ(r3a^f1$}8o$Z{C;)cIQiS4-qGted~ zI#0RlLzSPT>Oz!|BvbuZj+r(mG!3}}ACCh|-5qWq@_MBG8z zKptuce&K3?_up;b(ie$z_<8<4Mjzg$G`;AIp~^o&4vS3zSX(1;av}pBb=p|Sk8361 z%S>9;pQLwH+N!D#2_j1fJUtowq;_Vgt+w<>)L(mke*Ia4*=SSh(=Y(lQ(dFQ_eVG? zmztLvYn~3yT)zeQb_!(~pd?b;$2wrj9*pj2Zde!YN5rsBXWEh!Y?VfN%>`(pYyoJ5 zRj5^69hy`n)UE_I$0*M>TV_LGOw8K45z7U`bSBk5h@<5vgv zO*+SD)D@7l>(w4-?P~XGpTBkC;sozL$>2q%>ee^SGrOpEOi89rC}c zEtLCm`TK9Xa&morhx!Gu|1H5Ji*at)Py;lMkC|foe#pvE`B}l&q_fdxf#@%Mnr`2! za7p<%5R6I>y``)0w}sqJkpt|qEV@KhjmtdC#_oAN%pu`fuF`~JzQPfw6QPo<#b0rE zJ(c`UH)ec3u3M-vLCL|g>j(Zjv5Ki9tZy8JG5$apu$RBOEQSu>*ng{ZkLR0xdDG0K z)jxLd^L0ATp2ca1k{1mqJR_PTxy(l|EWBV^hydPj8NXuKO-E&??1~>u+7dmuy0jg? zq|KT~XhsqD4Wq$k0w^Im*iT@}lG~IpF>-#TC={Q@M{qf_`W3Qtw)^xwh9>owT@Pzzb)eFro6{OxB{#CIlv(~JjbtVu!-c=Ka z*WU`1o$czuuI*uL?kU7j7E_RR<7>N+Az;!;-KxH6F4+5wQ*)bYY4rGH8#oPir06Ep zH3|?A%wXwFP1e&l_z{6wxhc4em3pJ_XIDqxqXw(yW-5$S5W$u${E@`8H_x zy0^9Uo~PAl1@yjcBfw9MdVS@*(B1`|*k$t*uAgNdVKWS#!lIUNXglGwJ%{8@7R z$uxsLlKW<)MNaBx_6)2HUWPJ`pr6!#7{fZ}P)5X{XaxWX=>_R155oonhnZ$(9 zWHmSib46L8gD&f`*=38(IHf|Rn-OQm3{Q^lho#rKjdDP<6y=#((g_4Wm4*D0hfhgN zkVwwc?kgrEcPskai2N$d{Ft#Q;q}+!3dh1bzwktawy)w}^P820(R)?PF46z0zCn{+ zw%X!f=OrD)tdSUwt=bN8%gLOt(t?=It^0aBn4=_IwcAQct#FhCY`%x=PO^0qT7YAF z!tB>jk{(L=YXKne!$M#9C8z)Tm&SHlz^^tzt(vc7R(Eg@oc)=(^O$!~T0NQzNdi5- zZ{xZ~0BoUG7WtIxdJBpTX?7pXeM5^J0Kp8YPekJP)Z77#G54QNS_?mgHAAY>-IdE$ z9`V-=+Q>7T^^5ZZqX^1R**+qOrA}1v9iujuq_lyrr0OX`>vBAB9e;N)pSNqoQiWsc z9QUIav*-fW?vkhDSAt2xlqC0f_D5kjX-<2RXEZauu+0oBqb+|rHXQ#Uf%YHo?na_m>FTNc}K96yls5xb=MnYnXRBPe5Wc{c=~$D852tt zu{`!`Kx(hf^l7$=lnCNxrv3Avp(7fRi}HNcz)APjfy;2Jfyf~G#`HLWZzCzGCH?GQqefP-Im%Pt(G z3L=q>Y22-Z?5!C1#Jq{iPZV;7Lr8^pf#}ke*wi}a+mJNzuha-uu zp>vOt4u;g1cjvYqiui}0QtbxG(Dvr!Y_2{ zi?dCil(a1)B-1yF5YJsG)8A&lgG=Fyy@C{Jiw(0yOKps1HmAUG1fLw|95Y=9w2JDM z^sWIeYort7IU=b3H*4PHh^b{!U%`=?$_3*bTED)?21DV?I~eq;JurDc{CeN4#wrJd z=ojpIMILPZmDm5%!jyDp&0=l+!^J0P)0A$GRbleY7>@nn6>Q_$!N=d_*EO=Ab(Pqb zfWE*4my!U%_Qsv+6^r|ja@kI^EE~>*$6Y;b&ZiN2-#d~G=XUg=reQ9?7m6p(e!x%u zQsdfKtck&|DP(bwjOhukb94XPv#%|kJab(9E^9zqbY;Qm%IXE@yG$Qj^Pg&{8c~ZH0K;A^qg;d`6ztwqfgJ) zYHy~T)Al*yOR*a2vIDlV?yruvi5i4ANikV2qp)^MC-}P@z?=kE>`98Q-NW@hwY5;@ zn@gtM4ac2Or;Q_`-@AIkcd;fraBv6k&7idmx$|VN8fFtGCA1mpI58_81i8Lgg{{hN zRlU;BwxO8#Da-dYCM&rz6wg8^whDU8=WIa2;8Wf?!Pb4iVVQ0-hxk!nP>aLM zCb=_)P!#TX1=Ua;q|F?)un3&!+r8h(kaz>fW=XiE)SE-c;w!D;B@G@XaP|TLP-6J3 zVbeD`zsj)m^)bfmNcLReMK&Xey_RU4qEZ2D>sA_0V~e#7CEZxNV?_Z6bIwsVQ!3l> zi>f`Mg*hUuq%h#SMM{}u?HP_^4^&u5 zs4zEn<32fzlK1eC4021B=tjh19Y7ulEX1Bjc~)F@i7OxLH~pSM9Lj8wy!XA zMi*H#8hL&j@Ov4|1^?=fXYyE!Jr$I3;yS?kuu>H>bG6L;MrdIwe|o(@%(2wjZhQer z8d%LJwcIm1NUSuRgKyZsVeR;i;S4=T8sY6ti ziY1_ilr7J*JF1#>g18;E@B?1R$KpL+FybSO>+oi-O)A5zPr7`fV(3_T3MVST^=eMr``~0hpFt zB(mwZMI?%c&Ku)mGt8_b{XJo7f`~X{>hB=%u_z`XQu0X^1icwJ_LGJgWcoP)f;kFZ zg>7<9Tq8M_cb}Lb?)>g<2ng0F_`c4H<1! zj<;kTdAXIG?8cAD`%zUpHW-~J=^SOTHs^UCY>B~F5^|*!D`pd$`CP)4Uc;9{egh5m z#SXvFeadg`+JyI9BZXI8RQ*zzBx?8E z>V)c38R-wf<`1Jn#KhI=BiL4YZQ#{iMb&*K0X-HmJOv;Ah}U{66_CrDh1R(ZRytjH$fof_xKeHeBbFUdhB@Zwz7}f;LJb zLe?O)2I~*!%J~rsOXHbqs4y{}W7g`{L%|dKGaoJb_?lqg>rCLYrRmu=*&BP3R`IkN zVkwbPPf^Z#_>7@OvR%I!egCrV4182>C(s5t{Iu>4%fi~VNr47gSYGr2AGqD6jAj`Q znOV!2tVZ$2XBkg|-9#sRoBf1-@Xn~Fw3tv6_;|rRSqquPns2BfAdQ+qT#}I_;TE~= zW-~UYa1V`JK-f0}!Y%!mb1lnTT!G75K039`FIQUX5dtT# zkAF9;UpvXJ)$)&5#e0_%R6KI5RG}@3m|lu%w{l}JO~9zeD=&s7bD;yDun1;_V}90t zVB#IRit&7ik#6IrZLTfSZMYL2MwS__yXqL>s{KRLoYDBcgk}3~C1-FpQt-*WiD1T3 zzj215xnO@N?>E&%4$PsP&q2`{xszhReiWIUrcI=$9iS64ni6{45DAHhkH1+0%*Y&E z7#!MzM8m=bWIH)*I&46K?`V&Jzm(C`cZkZg`1EwbEcG7E>GOYwH`dTLtx|Pl8tXg@qA{V<(pbZlp3|bdk5_Efl*Q zF{ful*JZU7WqsFHn0AQXjtFmCRBhgwzGJ~*>nsi{KHiafib}}d*ZGp?JUO#<+1sUB z%BpEm?GDK9H`O<|^@*aRwIA%e?`|C@yohMDz2@hFtCysMZE2Gsn7AXa3_t<34AI}0 zF3zAV#1);gM&Tp0dw6Oax%!f9tk5&v$GxYa-S}jjxquj-ewr~O#YWUjxnPYl)Uvoi zI%MwTVLQS}Va=YAhQBavy4?E1eQd&q(P<&H85u*xGtq`z*eFhPKuX=~!azHgS3yAA z;^cnOEsGHv?gR7`^8jXb$H6wRdn*4UOZ?vO1teIl1kMZqz=W7o2 zQN%*A&{aoF*0hq^51edP>$m7f@GrsQ1LHOa9<$qFf&1(p&nk!|)_JjBWfX~;JNW&y zc@hvLot{wl$zPT&qTXR^lOyF>JYw4$Ehup%VvRLuh%&BNIeuSfeUtmk*G`sPKaT2%^ zhx_synsAe_mcdXD2YKI~%(=VG0PG%RR(0Rawbcbi>)Uc8=99oew3XBG4aA^G5rwO@=Vj!`ez=8kuy= zBEMxcF4~dwjkVt>i6mPEPKLdWA_HG>JU4{QT-0plTOy?c`7*Ab^S!cosToWA z(TDs|Ef1M}u?%?dm~zn|t*HBlaxrs(|4%zu+2JKH_9_D$RZbV4&^9OGF9s0j|c$hzCqkE=+k zfm?d7_pS~X#aHZ(Jj;)sL^1f2JTdt|?lGUwJS^8~a{Xv2+iVhUdtRak&ZjV3PH3N~ z0Vg<-+X`RZFkU^rtw$c-ue0!Vz$m)bxIUAT>}r;ps*@#lquWVW!e^U)!iG1tk*0%5 znI1Rxq{ajsvAR9}v{8VMxrh!CWM#EA7s;i>^c5=MJ+Kj}@9m!%Bk^3k$Hd3wXainf zfyJ*Kx1Mf{7mJ-Yw31fZYz0eH$d zgWgUtk7fsL6!%H@5AFCTTt}e-ooKM69dg?wGtsv0d_l7{Yklyebs|IX??VV6BbC#6 zS~i^Dy8h`ZXo<@B=Klx@2Nzm9 zyB$t8tQ8rg3B!s(-kT1dZeM3Q8LKvcZalDXAoFoDtPtQ5?e3wYq1~4;=J{nB-jA3exRk(Hf#hRrrF+?39W$5X2YOj&lK z_F2{jH#KAe&=jT)1PO1u>|Z^D^~N7U#QoL;nsDv zlsyG29W8uRS4nbPOZn4xwwI!)ICgr;uddJdxXeb-Ms)NluwZQn-?e7_P|A#XOEngP zkC=cA0Hhs1i3a@99WRKm#kvXWRU~G*v!U}TY02z{E@-T}oPvyVMVbSSX?E7rag3~m z!tJxgpMko(Uv?eP09GUsOPSlM(`;NTC-NZ_`$NMfXul9&@@E#Ym;i&>O_^l&NnDLf z)?i~c&*vSbHG>nP?Exz%7PA4Gs92`<%V(y7P7||tI4Y(=H8>~5sOfS&rpGPzB6fZD z#?c32jN5pJEw)(PMG)zzA?%Aw`>Z{PWoqbzGL=y&X~uw9$dg0WWTRJqGB!|z2QWV748bOxUWl<~qx zf=(tOh|EaQxBjSJbAxl74kX^Gz@U4Qi;b}%Io@i<_jIr4#w=YO=9A>lE7iI^L{V}x zK=RZ-0AJ@{;5wuPI`JYq$!mVk1e+3Jl1LYtP!sny0x^n8K^UXJsdN8w#8_K{OvpEn zqEd)0BP(kXMsgvB_5^MO0L>q_61^QN`2vQV{8TN~lYM2nOI=%vLyM^-v9xcS_5ij_ z{6&!-_gW-=PdeDJi$O{GJ-L!YKtZQxifZp95>PKZICMRd|48W1+#HMItB8yO2&&i2y@~bsdL!8t4E6M zZ!A?%H#zyf;)OH2dYjI={6~p~wC3~gO%^mFF7!~v5%q?RlMPS{)eIZrhsTNM$1rei zX8#imoxk7{NQLWq`@mA;b2~kix+psRQEUFg;=ufKg7X6e;fwtvvXh9t^|KM{eI##A zT23nZ-|NgtLyQzDrazZFg%pZM%J>8ed|xQ8=OSUV?_9>wTc%AScK3L6t0=f#CWrq^ zW9-MyE?aLRpE9x-xERkuRg-&t3?2{-NEh8|SFKKXh1H8u|D_}fjbA5}4FoDT&QRRP z$M9Ik3Jx0vD`K-l`j~fSUf7n~9sPn?nH?>z-t@-@q!rmWEtWsXO$~4+g+oo*J*&bU zCF5LGLCbXM5%q!OWcsG}#UKNRfuJ4BPl2=bcoF|gNo1yaJXXJR@9uq9O=>OwA~m+Z zwe*X7b()9FwuSMvp$eSxyI!dQsk<1KWb`&(O2RXE`^>CQ`q_E=Cz~=!Ae(5#4cgh*dpl)WbbzF zIYSOR)5(+Ysc-u5T0tTtQxd&~WzW7Eojg4wYE4>WFjP2w+IYgqxQMU9B}}@4EDF z2)58_nRj|=PeZqSQ6LkxP*}I{R5SIFme2!SL&Q0TlwCkmpvK;exAu2`aM&kyen#y>kjy7`BnX@~J~&5nO|xANGt zM6`s8CY&GALxzAY;N63!dElrN4xf?pc;ERb+Nwy~i_26eab@Gzq`E~^_d!DiT^9>F z7c|E}X#}Xz+ic%|EJOr9{><)(9H&kKZramY$AXFB5w&@GkE;EA?(rp#j1-@Np5hX~ zjFY+}z-%yd9jb6hkU_cX7g`RE(a8sWE4_T$P*pC&fa|mp8!GHTmRth?d%0{K@JLE* zwv$#DJ3f)EB)gXkWdxtjZx}rvcH)fd8MP1oYwc>K+FHsx_T?5G=`QpYei@*nLyNIP zN4M=8SBA2{^gLkVt1x@D!d3Y3I>!F}=l1Z#_f*ldm>XXF>V*#8wR*BeiUz*E=h>o##TBEPK(_M#v|ADSVC;fL| z9s*LP7xlZJmG^mS!N8{9n=T?1qUuOMX=E=Ca_D#RHt=yd+DBx6TFE)Aq1S_j2e2D8 z;|`KsCxhTnOsj2VKW4RC@WZ!Sf))K6e_BEJ;nE=FBk78aN}OoZ6_@-Skc8H;s?_*% zHu~_?K+D-6%#)d*@QV()1^}L1MZWc?Gtw&|T zc&+(ghpC`5r>T57Iq6gC$U19CX_==E!1&N;2j)?@H#C}E=l%`ho|;r|e`B~M%MEfwv*Z++mjCc1rV~Iy zaD)Lay(kk1AQy4uq638H`y-1%vD)$8Q!F5wEOI9$5FC-Mm8m^xt4m}Gx7?MLv5gbt zkSkozv4T&ND>fUOzK|U&#X*&u0f|xnCZr8!PjiUV?7V+o3SuEAGjd2@e)+Sa;mn97 zAb59H`P4c2K|Xjc`r?tQgSUBwwb^)s+drEVVoFyWNys$ps-3J!_O1pSi?cfR`X)ql z*mybF<Dv>2h+TI8RiLVeFc-Z13?_GKK zrd+hMZ>-Moh_No6MGkX2g}M^1AQ);toC#>KX(+cY3NA}~lg)KHjp@t#3iauK~YF3Qzt<35TlA_Lzr-HMLx!{f|og$l8=j}VNNdu(pfAH5YIB2cJY)65qFw;SM!s@h3PEU z`Hj2blMhl~W2zPBqk9X0r^%5It?Js<{$^=0JHH;w=lsSQIaHIzD>dVZk0dkGlC0DN zF~Yq!gvsHU#mHm1B>@=}Rpmo2soJG)JRhstP)n#aN#%?AH{v>?Y#(qP^Z%*|MyUAT zm9^>Jemi)V{lT%X`2z`RBJj?bC&px1ccx(8;GV;ru1)YnfHcRwkygEXFP?S_T# zC~RA^lQZNJvRG?{_Wt?&JQkw(8b)RCkPB=ge*t7-W$6o?<0<2`S4|e5nAo`_v`xMx z;~G1>1UDj9-Yu|_LW#tO>tG~TW=`q=AZIn(xh0$H^ zwsuD~Yth)7o;4|P*(`2Iok#fiMgW#=GX&SK1$eYeRG4?4(LOGSwX!PLd#gVazutRB z^Q|W&Q>VBIeAhk$Iw^@ReDM0bprzitQ=mrG`47s6A9pKkS9 zR`xLaddd$!+~`;t5k-P5S*Y<{hrCR+N?e(vdKb@R534o`M^lgS4VAl8vVxPp)D#}a z{C*p5>)_O|=<#R=`)!`e(5bSUa=D^JXO^mgY#ph#D-)`x%N7o^FQuf0>EJq`v1B;4 zsxpiu#NARmg0el6BeG{zA9RgD&7z35_lstHZ0-9W4dJu* zYuLCw6hCX-S(8Kr-gD(*A3AgbxP}2UK=9|;5$C3bv4kAZaZW*#FL)BwNG8? zGY+qN_EK&Ey(TexK{r(_m=&ruyTuH;z}0Px;_7#l8TF)n`G~wPg(T=fFy9_qNpiX{&b%F>;+#^q>cJhmzY}Cx2)X#@)O&OEQ==!9@{GM3$BsPdUtP&vTkMj zfiuF0?uo>YCN{mUt+>Ut?cYn)Z#xOSORdeq!z2G1#H+ckb+x_WvKf_HqR40zkJamM z+*>sGG5zaynr1nuNkKG{-|RN%*wQuV6BRZS?|qAyvY#S*U~#{+^&3uST_7P?cr5AB ztZ!z$>9em+HLJAMFCNvIb3y^3b95!PrVyaXH3ehaUHc`Jl%|F$ z8@Qb{zU3p65N_UIAV#HOmB-HqPYt)SqPnq*S9#*0NM7lCL>vy7r zMpmknRYPVEpMDk9LPOGIKL%fpgxHs9gMEGnQv$+$`;LdLvS+AAYR@TnJH1{{mZHHVLP}pOFe1#;}CdMK^Iw*oJ2RAlc&q7D@P5ftS85@=%Kx%%(*~V3Ejw4VBuR# zaod-z()Uy8TEN|g6>YL zU7BRAo}D(~%r6%fJK|Ly`TK7k&qQgWJ%ER$vg{OYo#zyO%ozCBhh15#Bns*w!pPx3 zUsAZ&9uGQRrU4Jo@Y#6uYHCd0Fz$BezI#rtCej>DUsb)C?c~SzR%N}49PB;Q-MSE? z_LgdX%670ua}79tH?_|(01NATn}uE2)w4-nm^P2r)-(KLgwS*rO1~1MKa70kCM}(E z{Ws!%?G+lCfO0e-w1+&h3JXuJTNB=O#{dFuU8xTh#M(HZT6Hwf1{pO794q$=inw$5 zH){ZDgfDW~Hfu7o?%xH-DIYX`PE2Dys5R&~&lxkZ?jk7Ha#*>VYHi{@T@3DSbut1?n|Bn=62U*dsrsY=v)3pgkYBU znumQ=@%8Ew?d7Vv$g^teLy92|rRgxJkP+ z$jaqgJB6~i4((!*8UAX^UE9~^u}>@QY5xR2cpl&&7-`~}F|sgl6*o?P;o##IbtB^4 zRj>L;*?BTyReqLU_gG}K)v$XZy5$_81eNg~XsCVQm`Hp40l<5vpV0fJgl|V1g{)xN zHaKLtDS7+g3+!^PEg9!XQ}^HBDZ{Gn3|JFYPL1}`IkbFo>(>T_4X^5r1XPW!eSlLt zp&IxDRz7#PT{dYp1H%DZEW!HZhH@`GotyK2gfMj=r}zvoKr&~0x%}3zh4Hm+!hGnu zAL2_X3jot)`W8Uy;hF$6o1Z@VhAd)y)buS34;B^hTaWEPsGA?swr&gV}{XZTH z34BEDey%(BUC<<4)_(~b!wC1R_&*T|sp`BP zkSz|Z8*Xrp)&EyI%~RcEFyvS=Gnr_{OPJelVL<(=)bTloCarNc23=Hn(r8(`JpOFe zMU;acM0r&Owx_MWXM{>o-U7gc??$l%XU3gc882YAfMHv{s|Vf*S*e4!C$I3%{;5?tN3@=<*mMheNWU z?zJ67WLPI)d7PR-mbn$oN$wvHjU}ZYC{8tD%58F1rYCk)SVyeUa!~HDF)Zur!k0tt z7a8;Hze7(#P$kB`zkDw7c@HPiHS?%)l5~3ug-RSyJ$H!q&MQYG#!0Arfur@PObFbCkO`Ui> zo~i`{K8!x&l4s_D`-{}lEE;ZiZdiYl`y70S3XhYwNxi4g=}-?%e{*1`W#0Tw+0ERZ zW1wuWZ$5mh9k((K|5LAkY-8Z=SJjr!0Zo3SIu_YEch^+qxz%Z@p9q8u%X-MIgx@cP z@g}90t+@VP7?)0Q?uJo2BtG+yR>CDuC}3W<q{)TMFxF?G5f#_B|I8%%Uux@heSkva1sgZ@N@IeI)DGnQ4ZnUF&h6 z&?`w?+_Sds(sL`z+N%iy=oGwO3>CV^4F&z{PbCB~XKR6IGx($buI&oYVGD$}zduFb zF=Pp1T7{9xQItF^j>E@8+h4OkiM)C4VAsNrAxS{IF5v(2%c?;Xk-Ne};FRI+=cDWy z^(!NC4WAWZtZcro`|Rre_TM4VuW&n!UM{?E12exo?0LRzr5M8P%DP-VRKSg46(2;N zrxe?XmBhxvQumuN88mDRqI@nTJRgk4kH|OTp>Nze>QQ4Riz(CKB78Dc#`*uNKG$|3CKLGODe0 z3)|imN`>OC1&X^nl;TpPxEF%EyOdJg-7UBmcPK>y!CitC2p%W|w|wc|`|NYhH{S97 zegB;GZ;fPRtjx#e^Q`-txAE3pTb5obRsVli&G=80V*?lUKfsO+%>So%Dz_x0qAOto zfM`oWI-^%db~I_cPQ>y}tq?nf_{zm+V$1kHD7|Y~v2dw>0Dl`B?xX zG_P{`El)1))>&WIX5%IU#|_F4<2+X9-9%2b9oryIAinWbH>I`6b}a|KUpDVCNL6uh zRC$_nCgmiiw#jee*0%(D;5lhHz{%R@Jz+RLR9Ts;GJxDoEF~~D^gK`T(odPsu+yYt z8JI1-xAX|=b*r#=L+$bmj1EQta_KeJ5+*|!RqvF{W|EoqdzSAt=#BJ(%NvVIml;xD zXt0?-CAAn<5wZj=kCF^V%HlrAg%P1rJ zy2){wz#xhDtEuUPehPi6@nO}*D$=4%>jpPKmBuqL?m2xX2R;grH1{wkdO3b?@GvnX z=Cnrh{am_TzqKg7Lt>4dT1Al_lg%(UkL6^V%QrNxq?@%UejGzMfGC@8sV&ilO8x%D z(OMiC!D{Gf)hILX0I+6y;GX|g)qFtL#KNdDoO5RCgjHSmQ>7i9X|BV-{sk_PuAxG- z*#dG!41(8Dn0=hfY|_ZPx@Sac&tkwh&iMwamo6+z?p{@kQ*Kk7qIyvks<%$$icFJO z0-F)+{%4-JE4i;i^{usLf?DTyj87|tw?@IBD}QI5ig>by5tGRx2gFAge3PH>0g(RI zh2}#-_>li_Kj1wr_N+%^tq4ykmKoJv>Z=p+z4s%2_LKugHFuPFUKm@{4z8l7prVS3 z0sRWJq!;#dS;vvpvUBPJ&K`Hu)J|H)cF-vMk~-njhGAIXjz`a~{0H7YRJI(;Q8Q$z?cc1C;+tX;- z1}pu_3HLpIWI^Lj?z|q6lkIdf_=uE$mWV;in#3hQCm0M1b#l72nYgy3(PU+j=nrMh z(=yU`Nhfhb4gkL^6Md(1=xWWz{NIxH7&yg&O6!JO5(DU4GdWp6+LuE6Kr5>*GJ|^y zywW8%MUjx>0p74tmLMvU968|sML&o8^I-Py3TpzWW%E6OZFq1q}pGOvlSPb%&$IK!84tvkF zftpoW=?4^%d*?!L@6>gy1sH$GF1P*h5 z7K{8FOh_k7Wqx&gktdyWA3|XeWHa&R0yd2J^?00Y*SDYNk(CC<&H&@#9eCTHs+5cW zzfc6X#qy_1EH$D(r>d~VF2%l&-FFF}O~G%|KB!EE`3BLxMMl3E;4qmjnHh>#ZU5{z6RSk_L~LT44>w0ZT<_+*r8P?Q;^*5rj;gl z$&eIuFbqoGuMKx~@)*$2G740RFC6Amsx&f)_U0LT8_wB5&LltFc=ozITlTTX9lKJqLO#(dSU}-J7&H>pDAE zC-=wux92-wYXe+#$gEfWUlkok2$ml1plVU}*N_tP6jZ(X#D<`lR_0GO>h%ub)bC zcTx!+?Enj<-1@yM)xl-t65e4~+a5OI0b)natuqRJr5VmfIu4jT+yd%R4OmFoSxqG6 zMyz4VB_SZZ*4IP&{+8+FCUaxq;xS_BlGybWVhH-|4s+H|&I(h)Z{9%SE2n!bEV)$S zCHcXOsqRaxxA)HVoLdCPX9oh=qb-MYUpz7WDrdnR*}P*(quQoExsTi!^@Z?6i>++? zp??(LCnoCVVjvYZ57T*rpD5ehQH>(}70RX@I1blN9RV}NO>^2IKdv6eLheHb5h`x% zzP1=nA84%XZjH0Lgo(5WPw9Yg4lGgk=2(CAEi(~GJ)8?&MqjacZKKf}n7NZq_nhL# z7&u+F>SdtqI5Wf}61HpFdbX{)a7EhIP3RQRqfL=?fbh^-cY|i|+6sKb7gdrrqNB2d z|A1O42>Dzv|61`_^E#8MaH={TAzT7YS7@>I7Q|XCBDl0%9L_H(L){V}}4;Tx}CVQY?A}>uIO@IJe^Y?U;hp`@JQWRWJDyX z`@!QaCU+km4$2QO;KEH0VfqYTp9~8F99kEnmu_kD- zqT6D_8K9%G>*o6>lyOQ8=K-c|az=V=aL5)O%sXeRoOxefL2_7>(G$H)mdDh`8B_J= z2Q~egBg{0tu(FzTbG$$g{0xOibgD)dEM1V^(V*TxXkxi(Y*?E4qvY}!{o%O-uD5R5W&Yz+p#)wAU=v@~ zAmHn&y`sKV3hJL=!*S-Wjs_b6!(oz4r--LtveC`c{=zr-JD9-S9hOvoSh1V#84=_D zBA+u)vfIP4o=7jIG8%;NymE)S+fJ5uGbR0(+hlpP%I-ZHfO&GJarcI7murb`6w z&cfe`OE(I6r%C||<9$1YO(H9Fx!q*Ybr;oOWUm=AGMxO0ixoBe4Y}u~9o?L6%C+om zX+o8@X+W-~TX{EqZjT$|X2Syk zgNAH&CrA;&`RJnx>B@vH;iXn;=4Ku2AF{fH+EJ%zbly_Z13@6Jxf zD+(&@&YKT)uJ^2%?R~C`!*I$^^dbA(^mg#@bV_j)YBRDzkWRd#wHf(gy6SIyLa~Dj zb@gI|FbUdfQ<$*3jk3s=7cmw!Wjy5lDcxee*?DpC+4#i<2XN{FD>^K|jIW)4(u!yj zG+mNw>1()lBmlHM9wG9w-451<7^WZ*j0Y-AyUA7Rtm|p_aqPyraP(E@O`%GrpdWWv zTRzOsv@XLpH6PEcqo#T_A@-u0enD!4Npd|K^bk>s_3<8F zN7w;e$ILQJXD2pWf=^I=o?&GF)M>2mDIi~5Wju3QAug8u zghUuVBi`7PV<9$|5{ySUgEsQTc(=}3QXiA?mVj(W-i!Q4x?quQB%JW9(xRP1+Y=RR zTj;gma=a-vSQch3_P$iGpailm-YX1{yheca9-2LKW@8I}Aovecv=s8kWJpu%s>&)z zwjM|0k0iribXW@hMT!t|YYJY-`-i{E^TP9v65O=9@=QMrTgLNsGV9sTTJu*w(7(#A zU*Q(Hj(5*`R6Bz_d#aaZq4w}fIgB4xuPd0Wi7i2tdgZ*XPP8Txfma!R#G~A=ByUGm zI%jsdS)Ep?ALNqe{^UolO=JCz(0lbY_PGJkbNpyv8;$B(H)DXJ&2!XIxUVU*VADan zmj$!_^#QeD_38?EbL1s`;?3tcosk6%VMS)X)?esUQ`#aFW|-eSM02=+`Xj`ZtQeI- zkX*HtNI>p+Q?q_4RJL+Wrm)D__}D|360#^t1q*^KPSmmtz@ifn8xHlY|JV1$i6>aFdl zzJ%d>xf8)@B3jH?)D5^DsJYFL6Ne=si!u#*!=j=<^2{1o5`=_)zlo#MG)fcDEalsQ zBwJ|hK1Yk%=A|_loch|G4SyJp8{&kVzxk@-rw*yr{013FuP2B+Jesak>a2V{h;q?u z+s{pz(L!1tkU(Riq=M*^Z-B*=dgve_#8uwn&?S4+5}A>unI^*FxMK?D8)fi2$E|B3 z6*gGkg^0ooFZID*?AGHTMwkRmcela%J^iiON?}#44+{-^r4rrB%6P?rZ#cluM@L?O z_`K3Wp)==A2tVgoHw)iN%-F_Nn!cNKsN0aqbPA)eKN#2)M^H`A#=_|x0}>*~{NIX=dXC(ohb%h(8fmB}t(tgb zSWpAtT`xvkX4s*8-1&2QEfJ5al7z@fl~-f$J0TnReeHAWm-U|r+bRd8zZWWIx!RxD zE|eLlcuH|tR%EyLKS=%IJ#vUsL4Q&3T*Knbm5T+z>3ET0w~)JXN-SRrSG_2>8$E7u z_riaTcbjuxX^|sK(Vgd~xux^YYw_~bU_-l3IM(i^AeT`TCkf{BLJaaKd;(Q*af6gw zf1v$tKz(Mhg_8G$UkA9pa=DOs@EN!v(S-PkD{_n}dejR6y8nbz1QtjWay*L9S3G)b zJ4nX;{rL8xf=6D7JP?V!m;)=;!Wk2#Z7?bCyYX+jbAfJ#g3@*bY$L!lIT#U_Hk0hKXT^b=2%RyM)oE`6w7+{-yHc zBXsv&3p!77Q;VWbqKDA!@@cb;n6G~zDuoKl@)*1ocFWk9D6E7JCuOFbL={MM<+pJb}n zUGY`6eX)~3#6x&;DTHj=xg5lR5X7(Gu z7fXM%=;*V@Hfn+_$a2jdcf>e6ayO~s2`p|+{!$#Ore>-@%4#-1YFVIl=+3g6Rv(lvZG%$OAk4n zHTsh>k1Z-8OB3f%{?)^#``WtIy}=GS(M#tyjj0KAO58Ix3Q3&mja842ss#p#tepEq zoa6j5uMmC&?$^nY+I5Uf=&l{Qcb8sxj2OIDk#9jC76)4eUXTe5qNv92#Mqi*@GJ(* z+itB+)ZP~)Ua^dlgWUh{B-0$tW^f9*wj)Y$|2 zjMjV@Hq9)*s@wa!DkEC#85AXT&4_M8mQ$O!A=~$z@VE+FqrU8Nz!DYHl0c*-9~DNK z_;pUy36qVg9v8vlqku^650k1hu~EWNe7E^QV`M?i`8Yeoq)(_Oq#(ry^^7yz){(A5 zM6JkbZQyQSGR+jrCtvp4>0fx5N-(_85Gv1uQSqJ|j7J{!bh8h=!a_)Ig`HK&K+!ul z3Mt|4@zu9di`O4n1?j+}yjT(r_~(b)uC>Z^P3_RNnDA~GCMdv1@W%tKV`$8NtVaxm zUBY;_;Iks*O<7--T8vCnm08s3iV_pg z{l!itw}Ne2DNw3QV!iYbq=w?WYy0?oUe<2m&Ho4nf~?=l|wz z?&6|l4-~ehRYYqfi^5Rlo=6GsSUG%@fkr$xi#i_-*Q=83yB|>zqjk$1gnxH|{i>1O zr+HXA4yol95@ZsfyrIZESl2Qk`-?rfC{aiKd?l<#IFz!qJrEe zxq<8a|AB0lux~j&5P0dkALE*7-%)rWmng0!l9- zDz&mLm0D6e1rF+^l4W|r$VW#lKu?GFLbj8&u1^*-8h}JJcxqz$*8_Vw_U38mA^Vc| zR;^#`Hh{v*ihJS-ah}1x#BW$MUg(}X`dPJK;$l@fx#u1AzQ+~CyQ5z%hc$N@8a8mZ z%zjDDz$R7H;46#(E`*TAtb^_3Qz&J~%5yk`E=}B2-pk$I1c4DOJ>`{!)E=(1*yvh$ zpB(W+qS8Zt6hTX|2v8mj`YiX5h1)bd?Bw#eC)pWyRwqg?@AOg%)V{WwH+M*n2I@^o zKJf#-R3ZYANCg>E3ual}lgcXNJX&#_+*;P5{-~gjTXc{e^6WQGa1;K$lI8jOJLfv2 z(5&ORxX=#w+sg>%XxI4SvQ$nl#BVOZl{rlizgGrLd_aU|jzCL1+URRrWX!K!b$<7x zPlwr0*nKw?lR6ltt}StkZb9MuQqq{Ay9?(yh~vs$Q8#hh{q`J!GCnJcM^CX%l&b_x zL^`(%&*ca}^sHGP;Oej&@%PwMw_(C~vFei_)LF~%j>v#d-hideDWE2-$nqYlwk$Gx zkDbW7bKaiX@jpXj#e|$(5_s%7Cr#-2JFcG-v~*)3q-Mu5K)jIi%Ot%m*(6x>%BijV z+$Jp@_;HMePf~B$8J?((O03%NafLTIha^rUA`J8_*zg<8g#hvRVs&8{DH&@Q(m2#{ z@iB)LQkDyv(A$Y4%Vh7KJVQ^fM2NrF%;L>Ef$e)?D5`HclAGz`A4pGoYclhz@)7+4 zGlDciIOU9lS(Mc!?8>$Ns=Mov%nZu!QM9$Oj&XeF%6W^)^eCKGiqUVu6Dg@FFd4%% zeN7%|rJM}xSVHkY6O81rX{S0$m0`T`#>|!lR2d=wlQPl5_=~F+EjMIu%F~J?gjqvv z^uFBEX=Y3wX(vthRg24z6!2<@%{37XKPOm=rtsE|6mFu!(6nE2EC4P|2vyD5|1t3l z+i#Vj5+kCOzkOYdfM$AF>_9GWJ64V_-uPWeU>lZvqG~^ zS2e@yFX1m#Y{PzB05ovgRkUE(yEWx`w5oN_ZU#3ZYOXoBb$p znM2nIEqhy!i1jM8iwN`*iMw1_r=@fL zHqIpJN>YrgR|!f?IJf2lhLiZJ>IZ-m76EN|ql^8}AIH+OuK@A!yiWlZVXwlYH5~7U zqWiX7mv3crmRdk5EuXVGpV=6KWsY8xout|!b(SXBM`LYRMAYU$MXNukla_cC>;2yE zwK1Bgt@UmP4>~k%?OoT%&Oh@hp-d+Q z!k)LRW`+MuCu;F=ezeWD@+G6~8Gp5`)WwrTsq3>H%~!f%00O(>fJ67*D%@l3hWZ59 z#r9YHgTqEX4(4%h(TGUv=}otY8;BGGUkJA~f#1u5g(Sb=PVKjsJ~@qCqjo+85m;oL z@m0&CO$FG=K8;dtnT)EzZ%r)-pFw~Ov8rKKVh#aB=IHHsM3;*1d%k?MEvuH0*m+xs zo)a?^v?WN1$u23OuH~@Fdl54!y5{j$W< z;rzWK#o3dVRhGUH0$W=;>+@1E#@uI)xpVRg3t$2o7|!I|E#X@9gVkDpX3`?Osjp9A z{KUl2?5BZU+`a~#28m{+KIq#SSvAMCm|kG5_N8|}&Rf$Srigu2)FM}A558@L3%|{+ z79Xm4rDCpXNQKk&unT2nV94vS2e=R0)SqDzh;6(Fog3q2t=6a&i+_ke+S`|k^PW62 zdu^RmvwN<5=?5+eSfVFpjLdj^*(^_7(z7q2mF+%uZg~}XAvm{8B9-NXWCbh~2O_H4 zRrTP386haY`wDE@Ergk~tG;sh?e#j3b5W)U#~ldcIoqK3R|ZFt_zHWGb%O-B40s zs)Sy3IL)*9GZ!7zhOM9;*@d7auYm_EWX4!M|l& zDM3gyqA;hPnMTfi4==Zq53j~SL^@CD`DnS7jlX>8x*(!Itlh1BFu%9&TL4wLvr*!;#xf6NROJHabfVpgT;k`aqcgNDfa@PB z)WhB>G#ZS~%Ada>c89b$QiV%RHdV~IfsGnCMq|F+UbTtl1(+1^ERnUwnm3pIjBTU= zaWcb=l!SI#_|+X4G8S?^XNe0qW?`O4h7;^Cs@Ch=)%Xe(hwAs!8{Vp7rMgMkRu^Ry zJfumH6o61jJl1$q?;qYDN)phK4R)}65uA*Hbg1ZhIr8hYlOew0D0!s@i|*FM5pk_W}7D~_!F}U8kdzl5@DT=sd?qbE5;yyl+DN( z_NrP0KUs+V+nGGJOo4B^VteA7I0)1__$(sWv~cwe@d*HOwjNb4v8GSAa~f4amxB&)29V6 zS1nNFGrr5*C;|51BVgPkLpw67cFQ~(eVna>Uy%LQ@qyBjP*Dw`gTlx=m&XdO(VLx} z4JTFp3jv_cdY$*z+XdX7&M(t}~W1RRb>zLBk64_+!zk+4C6fmy#+lg#*zY zyn%mRygZ%rn~1nTPtzOOGLaB+S8blX=6}+vO2qvrSCMPds%(*#fT&f9G{J}=CJA=q z6V$?wiI@!o@cPJPBaea@nxHy!o8tzKy2%*zP`lL<_P_E6o`URfRRpczsEtoSG**i! z$>P1F!?*I6)t$+uxNb}=dPMBJ5p}{#coqIR?dWduM!s9F%%t(<%W>ut^sMX1{QHdX zlN0l*_L@QxpPTsMrlvLm@X>Gu*hLmMuBV+?``uk$Wk)}^{Zm_uEUuHmOzrj=z8(-} z-KANMs`_nZAQ)GNIik&=v3$))Go0V>MF}&%Du|mT!G|`o4$z5F!t|}Ec`C}C&ZO2_ z4dTw+%ZNB=4o_ozP*#4>vI|V72eeCMD;sPD(sP|>GB&o>^>a?QAaN&M&B{~Z6f=Xc z*+z9Peld?{%>iHdSkJJb5_)gb*9tdori4XoYgdfIvMo0Y0gk-RKjy+OAlt#eGw65Q zuvTEXpz6(gtBLtn`U;WM9ob#vPzd#0eerwRXnb4^e^tpG9v3QsA{qy=XdZLTJFa3!e zm>?XI@~G(3kkt(>jm}0hzJ3%Dslf+vPCUA7r=j;3`vd^if4q|q_WfD2_X$NFKdFZ( zZOo3oGoVR!lrbp+aFP4V5OYt6tAOp9A}&;c)tU#1yuH1bj`E{v3ADVDmUBVH#zAvg z8kE~}()DM=3p@)H*f)?jKQGy<|Cki+yhCWo>@gW>m0hc6C=vce5YK zE<08^DRCo}Y1TV_k5A6`v9lzxP~Vz@VJX^@|E5rpwZR8+%Bh9Z1GbNCmi+eEuYbv_ zc0UHsQRG92?1gJt)wXj#8^n$v|=6r?kTo`+V;;6qQLb=b5jR5)3mEUZKmn(a}OL zcd%K}eS=Fw!^UWp9eoC2*{*n*Hy%5Sd1fPu)TVvJ=iXuGbdFD@Y%A=}j!i!%4Cp9* z;dYD*^sFz1JBPFSX_HU2&lQ}dr_=L2&?~8tfpZz9gN1&P881|IuOti1O6uCPNcTiO zh$&9B6c7ee9Y2YEQ+~Q!RzT0P@qDKJIvQKDb1Y=@TWL*A8=4iZpJ7zH8W;Oknfx{1 zE%H2ya_Nx1ud=G1DDtqGYx2zDu--QDu&L7=7=;CHwO|q>J#Uo-X_ZTs7e&qorn0#Y z1CoG;J1CRgIm?81`BAs`H1s}3UC*WZJ|E`egQ!gWuE8JUHHQZApt1yz7QAxSh|_Y# z0+m1EoLelA#`TvlCzjQM_!R9IcCH66ax zWVP-*0|7WE>uWj{Z2!ClBttfBJ2)a;&5t_mcgQAnk{NFl-Z*gnp%QuEN<6I6vc@7& z#$nGO>C0wdfZ~d0W~vf)LuzO1L2p!`jD1`E{ccoaAEUzz13d~v;3>u^ZMJgJm}Dl_ zoZb}Sj7un!AxWykwDHAKB+7~M+S=@seqZ4z)Zc2Mq4_CXD+40}5;3lNMqom_#J@;M zl5Tt;$#i~iQaOtnnygn*5-T5~$DQ|=9v&K#?3^@)q@NVXHoitq%-lp{|OWU5__dK_mG94i~xflxN3Dz)vHeMsTAoH|$ym&mZcXk$_Sf zv*4;5J&u*g9!hIhhB-=Ut6c|w<~ttxt^nGIRD6@EVek$u>?^$lc)n)E#=@jD0l^6y zPQ^!VP`RWY=#UtM{fL;sONFO|W;uL231x)0Ns2}~O(m>YJ@ppV^v%u&!C4!B9E(O; zblnKSHH0 zLAP%53=-VlT1?U3EIV#c5x=qkrD3W%FG_vXl1hevi57#P{Mc}5paprc@^jzRaC9p| z1e4PkIC#C~=SJ3Wql@PLvdFZGKR=NdPExlFodB0wdz`;3^Gh=_m1&n{NhI}VIdpzl z8zUeGRP8FN2s0X|zor{E`;DhW-a`%>O2Xkv`hr-1oh0D0&$0xEi=9Y<3=~jz!n9ah zdOXmP#*UAo@Y>61{%w-)(YqU@tI?~wh^$Tg!q-XAQV-&(sGa$HM0h088f~ zrcq^*r#RR9Y{+U88aWQK62+xE)qGMUJTiS>Xu8^A3DO_54xC0K0Zx}&@4K}H?)+&q zAGn-lOoxSN+VQ#%b&!jUa9-)>$V076s+fvXzgo8L z%*Vcm$skLq>2a>a7{%+KV+}+!x=|@0L3brG?BokRW>jZdB^mSg-)jf8joz3@*U940 zNPG4p3E1+;A!|SPu;I}J)R0e=OR@Cd3$cP!e~Il4M(*BaP?IeNN#6Jyekr5svh+jK zsY$+}i5Wkp($;^6t}G|MR$_bBi(IcG0=4IY+!tjUaUy}1TAgmz`=8b>lFwy+!!$I~ z(UJ(Xhz~l=ek^#vTnK4vH+E;rcmE#-oG~LA zM-&8--O@ya9^HDM^QlY9PMUtr1LAprkB2;-U2OAQY`Eaw2WwkNXN`2Q=x2x7WzJi7 z-5v?WK3MWtkO==NWuL7{Ezx5869OpRFWf;w24V96;bHVaH@sx{jN7&bc)pw~r-pIc zYv7E}Rzmn@S#*HQVH&G7W>Rjvdfvh_WbZbsQ;PL6?Rfg&w}8UresZ{vE0-mf36j3E z=zI}o(Ob|f?1u)LK6( z96R)az!!EOR$*na{=LFDMh}D2j+)h`j^^Wx<59UJ^T(S+7lVxBUB9bzCe6p*PD#nt z$L5e$R&$xRo*YusOe@LJwZ2XBy4i2&croJBPc+_j3i<*M*p(blz#!fd#ELVs89C_j z;bS7mAC8znzAin)LYz)iXa$Ua(-{_S?}|Sl^kbP?$qYcgLv7hQjq7Wz!8KbvUcU9L z8?Z8C7z{Ma;Q$Nzdy%|25Cj;~xz#bJZi(_z2=yxT*X3_EWD}wUT3GT|A@@l?H z&s6=50uWvO6;=Y%75|^LIZG)e$+NG)_0V&#oU(#YuaJA+G>efZk-{YVl0pF4>30_U zO`a(t0<&Dwm16%;Bk~wlNMlys}*NbofpfmTVEN+a0oh z?4{T+bc*q^h`d{PlAw310I>@ac0g^Mbic*Le?OBOXJt!N0+^Y5fd!&U9(9-gHr*Zm zmgNgvNHWv38Pl56gZ$ElgnXfyxCh~a!mF9QOgGHIe{`JZwq zdBl9BQq?jY(4Ad~gC)V0i;3$s#nYyh``L%(A=xnI z;sNQb{iM%dWxGGFZb@&xh*v_w38hIHN@1|F%R(cQ)|hyY`3tSp6!$>AVJx%sc3y|TZQT|a6EIq-XgM%S5Yy_z9vhtA5o?wJQsN=qfg=6j9{ z{2;_?#V>zCX%fSR$iQjG>jK0w`vF|4SyBluN@3r1P6`D;sv;Cm22!c4=6tj{DDOmw zA~H~(u)AAfpYm0Izj}Ur!E5b)FQOS@l^$G5+DO=_{M7#gbDJ>wVlnZ))0MW>LV(LD zjbf&q(cyk;=c&Vj!&g`o>>=79f9Y+eT}?86G*%NBaMrOik#%Y!J8s_Zee4c&fEUE* zQ62y_yCz32M63tuKE*2|?OIF*AM??6H=<8FB)npKT3e2qzagHKu`<8_Y(@W8ubk;t zE(OTgf=zP^RWCYt97d{UTGN9jQ^k90wV~s;!LsSPAswmK99roh^=KQg5Rkc`3DEwE z#^bdNM=@YUFR^#oilpaS5-4(6V4^bT=O$VwY#HB`ymDHRlsV0N%AIjV8uakKL6=); zpnTLu++r+C9P+0zfvW5WRAl;3jh85c;sI(53?kkVzGT2hpY2vIACb}jn|2_!C6foC z_1uvOXkYEUU(-?nRMkE!I#?OZ&hzzy-tQk{>jb9R1H>NYBD4Ed|s|SmJ z<%as{=JvUyBufR>*eH0ph9>Pov23>)`o`la(mIjVw8I7I5>YB*Q~CacIyT=_<`;$_ zzxU3k8?IMDM-4WA5fmyoXZWZekCAS4JEw8Pv-1?xg?IZWfZvvPU+%Vn%I^hf#p(&C z9U?1K$*EozjBaZBxU^vh7br}e1iB4kToyz~y8g6|6bfCzOI!4yWlY*RY{oM9#b5Ol z3v1FVAIWnlskiDo!fHaP1zwkR+Kb7S3h% zRP2@F@F>+H#t+tySTELuUN7zqsg4cHND_1)8zd}j2~Pmz=n{bFOGz6kQIyw}aY^Eq@bmx2UFiSYyQP)&ZwJ|r6*IIypWZt^!usdx*Uv;l=gYQ~+(g<%WGBAr z&@a{1iz&B*(yypfr~dObEY-@!p>@aENTR{PtiZY;=RmFhG938F4kV0zZfT_fI|ecU zK$IO7{D3Lm9n(R=bR_`tnmBf;M4^+#J zcP^4J=)X6^V{E1v=wGSnfs)PAltMJa_j?I8BP!THWt~v%6I}sbJD2=?c%y7r9nMUF zeYwOa#3BD|P|F5qBwS$rYa7hrC(ab>=YW&&b2@r#?u`Az}6>?;ubacW@*=!9E=yYO+fjPmOU2`S#j** z$J4<9|Mc7vIdl)VYjxl3EOCkR{SjPOWac>3T%n{6{1yLRom4@375>}euatFErm5g% zKL&2zVX1!VKdrUcy}y4&EOWeE@@VTvzr>ZjMh3xPffDe z8R%Uyjj#v&%esvU_-f*si{IBt#!{kZ()zPc_f^eMcwNa$Rl+m+PJ@=wp73}*6w7J8!g76TW-AokHiNe{cvTHxOwqZEI57D|?#*w_8%p;~jyQ&V%( zzbi;gyiN7~_tDDQ+87UTJNlY_aYQHVpGVmJ@!NI`$@5daKvwzSc!8pf2Q;-QxKkLG zC*~oWuE$n^Z)hW{ePI?ItLK>jP{4+1Uw>YveqTJ#jTfSV2<NPlJV__lkV(W~YCzpScWtN6mP#nqPzzMF= z{K?kotlDtK6Lrj@g@=7pNZDE8ImJaZ1V$hin7=QAu*g66CcZHWY@O<&HZ(gK6AO+n z4X|-0YJ>FWbho-RVxGQroyvJ;W&M7-pG*&N_0I!^=??xjnfguo{d4JNbMi0K{-@=S zDr94w{h#=%lK$`RuVDNy694KH|BJ-`B7x*)|2H%I-3|Wlk!ag(KkkAL?zEguDL}dY zy`o<1WIA+-OGrFX%Hj`n=)T8C?rM705Dy-Ay<&Gkf?bQPE*7q^CA{$P@RqyNVQ4#j zT!K9kum71B!{&}w+GpE?0aC4zR|6ytNi`cJ|A8czuBdKfKHMJ% z)Qqu^+JN=@5El-J;xW}_V#W&5fmOCH^;{za+dQQZT=U#ZPwiQ#9v#X*3r!f+%*Q9a zFU{J)7kWXQ>>W0-uhtO9h;j^QYh7jM`hP4rjU-@oY!)8&O0}k%!;+#s4>Xc=78|Dj z`aj~=C#~9J9HbFQpf%tmDyVlxH0zcegzzN=6(A<%{uLv^@!7dKutilt(Pha92kD@# zj-C~PnCzvW9nT!%5>zU3)2Z+LqWW%L&*>lzt7lW+xLSlkx|hw1^p zp(UfGpaLO2z<6%sj=_V#6CwtK1_C?j!g&#@PDPXXyzAWj-L?KiG5?6svCX9)tn*Zx zO}Gu`s?A|)jisyZ&r4gms%oKzxYi3b{pBQ{e`WXx8DF~V<2-26l zdZ+<8mCO0~%gx*GI)`5fV~=_{L|%In9TC+&z0tM+&p20-dsO+(UNqq^N9i~}WizYe zZ+cP7-*oJ>KVH(;9icD*1sU{p$47i}yz)S45ik3h&lP_AEi&l$?)Ud4P2qn<`Da(u5=C6aBEl-F+g=hFxW?<7 zACLR$L|eHg*3ayW+N<>d#(6*CPGD_l3BjUDw+r(}Bjp zoa|Z1@nG2LEp@N5nwymLws}{1v8E2;Il~*&8=xPl!!^GVC%O({KnbE{`@Vn4s9Gkx zs{NTE=%cU4$F`JDOKN&hEu{U}G>T5F!*`2ZzYy5L&YOc!XAY@#&a)B1ui_q-v0bE} zgk#U%8KpY}9Sy!4f91T2@mD_u4svsI4_fqe-Rv`|YiVsJDvO5SwAg=8I=BD|_9nj@+v`nipuiy$UO5Xr|u{nCM5-xNsm3PDVG8F6lzH!?3?|k3- z7&(Pe=Azu*t|6*d7k%`Jn8Hr;JB$`%l2vXUSk1 zX9*10Z6{^YTrzKd^r=?19b0IhwO)Yv3G5cUK1I94-;B0eXYIIP;@|nB5mV#@U(d?S z(%dwmvvi`i(kv+!gX^?>s`_hQ&H**axL>A0&aO(dFJ=<8+hC=LKeSVE(K0o@-U(Ho z=PL95T399gx@2K)yxOL_b}Qs64rYt^wO@&Lj1f*0%b>k`tqJSW%Es^>tJgk$*N&vBq(>95uzES*>!?E(Pl>&7G8A)Fa~Bd^URv`67ma)BP7 z7N(jrRru8?wqB~Vqiu$)#L@&l35rl_)E46-==MFC##A$DsXZ@qqGv}(V;akT56eL1 zIFfR(Zp={YX8N8@{qhSc3f^hfkyo+c7B@vS1^%a{$D7~3ZTOI`G(Epuws1h-Q|tD@ zZUx`X(1PIi@yfML>lQ5*WwVwQV$fyj4?5Vw?3Mg}xU)2u)XJnDeUcArTnkhP=!NtL z95c|s^?)HY@91?h0{N2-%(=8y1dOAdJ3M@S>TLP@QAKJCg2>YRE76B`ECQ6Dytf9)_)O%cEycivQ_X)2qK>2fHF1N}kez+WFt+c6Qda+`NOESde;ewuLsI#EI|kE&%9=D;e*Z~hn1U^P@oyK)Yn4b zH1u}6sG5c0dxFSmaFI2$*MNsT1HeD%HR7H~^!DD}_G+|KHr^wd+@PNRi`@%O#ChPF zLf`c^6X?QFAVo|NZ5|9GTjN-B?3y<@GocZIzxJ3kr!*QxJxQB=oJ%}g-CwuHLuq!^ zdlfx6JlpL>*o)R|k6M(?Uqdr~J$1K~i)Vc9Lt%UypaooxQv&f302el7Ub7r1K9`vv z`BC}$<_~wiGcv8OP@rfs+wdjXajPz~m*R{7OyhHvwubwRN^0?&1LA8sN@RoY(|BsG z8HL(#RH`J+Vl)AK%mnV97dba#BUWOoaOR=_^ z5GrtZGDlbYNDUc0A_a8JGGN*3^mpbM&+8^x=zrBqVKNv;IXE;_>q{Si=)v%Rvxb$E zeQzYI>*v_A=g{@2p9$x6L7p9r+e^(FqZnZ5QGZ@XCWd+RK- z(WOH@D-J6#Q>fL9;dZ~iq)NKqMmB!IXl9l(h_uqsw|Z`}<|h!$pQ%kEy2Y3Dquq*6aMNl53EGV#%{#5x+WTixhm} z;%mGzS<|?tMT`f9Z*2CNv~AN++q`E<@Pl06gZ-|m_+x#zK2EUkJh>dxJ-S?#Fmo`j z=>MFlx_;78Z~F4tiYrT-feW?tm|?UQvde^ia|fge`5$zBr+8q@1z^$&Ck2v&|L_l0 zn4sP?X7;=>M7v#NU%nlDdRLe@l->U*>N?iusJ~*vSB4REv1#K3I3i^Z@Q!m}Utu2% zkmI8f!9lxrdx1UUiy2E1GSXjKW@dZQIf&_%p5Vl`-EpF)D3=_dK6PXuvBcxvqddT9 z#qr!PG!pLVi}^8$lL|c1NR|CSt&E7<#(nrUzRzn27X<9-^T-<^)Vu!RH17`5Dn|bi z4HPx-nU;0xsM#>|dv|`&xL_JT%di>cwkOK0t!a7{*t>Fdk8X4)uzF2L$Yo9#@_Ikg zxIPsyMh2{mIO{HiG!A<1iaFMh=RDk?-u+~{7FG$xRrCTpNg)u4@;PoHz)&8YI=xF^ zwf3iV<=t}LdCC&oznPG?$MSc|xB(Hnli2D_Lfm#EYy@i@*Clk=KDu8xfX)%l+5gNs zj<-`N)W_Z9K6HgeqBy?>yGbnaHArRL8b!I6o=C{NKFuuAbeqOB-Ghf5-Oca~?v=2G z+%NN4mf!sR!_;hW=STYUXR3e#VhW#Q$D=RCy`d=CYT%X^goP#Z^AwwEkrZAdSN*$r zuIE0-t=7J})VX>u1BCfvSbjVffDHzqp}7cc$F>n28Q$Y&f^AMZDeZjjZhO<F!x5S>6h3gAccxMyDPB(v}P zN_Wm+{_sTS(X^<|$o|wDG^Nz>;DOEPQ=$o2v3Sd6zCfDV*~{&xN5{9cy)(F>j%Q)M z#CgNNP#ulFv;J#F7ZvIbzJN8anSf%*uC@}DnJK_)uo2_skKO&nBP^989KJ({OGH`h z1?So~)-lx38p-z_*W5{8pB|Iti5jiPUE2RB@VuTuOrN z&z$T1|FQR;VNI>=x~NkpHUy>xks{2gqS8^MNQ;Vq(j|ll2q+*Ry@+(8BBCImLg-a$ z5+L**B3*h7y-KfvKnMv5IRgd1xz_$xIr}<$?X&i^^Mh+JGV1$2<-YId$rvuL6g2L4 zB6qyb_*c00j=tmUngS;?_kEcKHP2?$RVfe<31_HNN};c+m_b#V6+^OqN@MD=Gt}U8 z5emoQ7K~5i>&$!U=jX2Hl2=J7M%kaD|rOz}!do+9< zZ2kL9*E5TZK5tuoBaK&Cgg;(>OUxKe#u*UBUZ0iH+#{aX`wFe0rSi8;DGsLMbPmu}QP$42{+^^GZ&_`>Zqi3zPBZeRJ2GYl8Wtp{Xc7zR*Ld$1w_HB3P-DwCxK^>!L$TxlSk-B zaUq`L`ZXc0%Z^RyO6fZF(A*%y0kp}Nk9!r^E;*b|8*t{*0uAJ*xJW%O6 z87MLZo*13OI8R>lttPS-bT6a` zi#B~-c^YAO*;rBd=1q3sGdG6CQef@za?{2a(wCqEFjYh*<(x=G*(2|(?L(o!I|o)i zzFIrI-Ng1a368BUiIA^b`&yManPAdcaL@*aEgQ4F^mb%|Q@tXu+&;TKnRE23dCeJ? zEo<=rD5RN9C^hD)gJjdJ^4dyFj4%F$EHp^*BJ8ar3+Vlo*(@S$9C{*7nw2IOUWIS{ znpM2`%<12J~P}`nn^a%64t-AGhz^BF{a)c-VqtULRmVU}D8kSk# za7|u4pCxzCV86{SpwQv%7hG4zZ+orPu}L|MzogKUu2vGbTYWC3o;ZvjoUA4RS#?3d zeKh{?I|G=*!#PJ+`7}3t7nQp_Vk~6ow*jX=Amj%bHp8F2LAs?={Bv)gglMd7e?nSs z4===s5YYw?K032-pTT=Ojrx{cnZ2D!w`LoQdnc6Au-bXt{Y5bIp%lU>i z5Klu=d*AoQPb?;okq1koMcj*;h9)<$D8u^lG?c3Ollx@zOqyukr%VTY>p2jEH}U!C z<|QSyPqQ0DI~$*QA2kCcdWb>qwDR`hGl#q}$0wEAs`Cg|z)ykdk?AW{A{aa?S<)+G zJ|vidAj92vit4lx`Ve9mIGJtb5`)!8K)0-~U2P^<>Wq{fg!NPTm}zTQnc=N9G*y)2 z9D3|fx}pz6q=s5pIk_0Kd79j!A5wAb9D=?O?NO;|vOMd5cc<@5+0y+FG37X!lKAU( z2j7xe?@bn*!9Dzp{oR^wEmWiw9#n)Artz-0Td_(&1F2hRh?~=fNN!6x`pgIJ>Fq-7 z8y>RHBm4CL#f+U~3H?l}!_XkL5s(BZ*NYCy-#`j=1{dpxVFs3)oeSGwUA^Z(tkv`t(~UxG#}jk*2Z2la_ja#S4O0ymrn=tT^uJ6#<-0 zhS)gif$c#;sq^e^)cR6Xq)Rh)xAo7%TCPY0_W)XESi4tCcp)5kNvG~%!piot)fA~) zx%UmYq42g2Q8{Ufy!KM)!%_fZhH(sZr)_Wt5T zd2xx~^Tz7fI`GE}M?^w&MQSoTfp_114jSH`F~!J7;%xso+roV|ziQz_m4f%7)&@{r zp2cxdx*zERN6Yc;Q2}L>s_vQ&h4}8Z^MqKX4+>QTO-wh_qZorXrM{VMR~ar|VYyR` zE;eZtWBFcngfYj`FiS)eoStauH23O}2+^&!XsQ`+-DleAOZwAu!`Ej>E$Y!5KFM!v zx^`NdsIdzkTnXW_?}mRX9-pCKBrI0(Zt27i^Gd2$)zmOOZC3F=F%?L~J_yk>MmJEm zofLP*e2*AE3{kj#)@3mwq``M)zV+*P0$E;ItIK_7n0$3~_#i1F{lgiTR7R^lp*vN+ z7eF(@)f9buyLSbiTqO3D1K2e-635{w=tLFQCnd?T4+e*aeaToE-`l=khZ=KxCRZ39 zj%Rg`p&GUmuX>VXEu}`8`63Ag_nem@V#Dt~xxcNNgR^|MC>|DUTR%%3>s-$*eD;Tl zWAw;jCW7}-rS6h5_1Znak*2MEut7^-*BXz;JWH;6lB954NqFELflT^q zk|XqXy~c>cWTO9CQ<~G%n7PE!0R}z4<_9-q84Ne7*y`Zyk%R@AZ}_1H!j@0?NKc4q zP|@ik?vlI0goO?UzM4R-%G{%Rqvdx^$J6}R7OFqpyLP#m$9kVdPsmHR=O#!`+IjUo1k2lH61m?C(~aH);onHb ze_?tk_g7v(W=oQRk<=+~^zkO4+~hMSJmy$!yjttm-cyY$M|Xvd(MN(^LuQ_m8s4mo zE1M!%WkX}PBjcu5Zuh|JzO}E8oXkAckWQ5{)*3Xtc?wHjuK9LUEm@<9@}-TzN9(lM z7Y(-eky=tMSTAH4Pd(@RFRr?imFMb~xLxUoQ9MmYuO^#l^3`Y+WF9>Pj53Jpt>5iu zrJRLA*BVazWU-wLfAaAm6w}>BwKtQO(7c8~4WQ9N-n@CRN%6;1zvUZ#J}>wcUUOpW zn7iwEonc2-OyhVQ+<9!$ZIUlB*=>x)wnC(LV^YiTwJ>PqlTG9P7Mh6g$PL*8Chj@A%hz)=+ELWR9+>&)Fj#sintGt*yNw(TwZJl8tOHnq%2b zcZLSycPihFIUj9aX{X z6c++d*ew9eO1fR|1T&aXl=;$&+f7)OTlVfG>!$YE$7Ut-;u&%ML51t1eiFod&h27b z#VkrqbS7xeK4J9ju{A(EtyDbx4*qHawp|PZYCsA@>c|dmKq6m@J9u{%1J0nuNkEGU zTMC!Jfm$-1??B(Y46wI(&h1x({H{Boh6gwo`7Vbo^9==&UOhPvel;iCb1mtJ$0K&C zLqUHJ;~CHM6QYeZ+|bL3rUDvcyN|5|K0WaxZj(XxR8;Vs-Nf3wP~AjsIFPmZmOz44>C5491^b8OTq6bVvH{-B+`4;JM&Rae-U4 z?@w=S4%y>wYdj4$d5S#x)GOb4wry^(5G7r_8+nDt6kq+l1a-!L%lG-utJ?kR81SpU zu>0S;|NGz1vH4dcG5523D&c4M)E>?;SyZE)EFrBZb|-}V#?2yp|K8n*9-Ze4QG$Jm z;TsL6u&NcB@pnzwv#+IC`uX<}u8R0Yk1x=6)HH_~baJdx(Xhsmi@s?bmf8o;hDC4&oA|f$y3QtYJXdL#`)E)P<8A zHtF;2{T1l5p20i!h1b$g{9%#4SuG@o{-l~A?&Z04AlM#Vix6>b=G_sdIi4ybZo5)f zuh6DXec4Jo!R)+mwpQ1U7(aT0VNoySI;R^uSk7jAhI)kgjQn2_kpm&K`NGwTdhLN0 zS%|DnPEHXa*y&ILqY%KYwz0DBUXugYc-qfQE)C>6A>QX}RLG`*JT3=pl+0b&fX#po zbt$Fc!st52h8a7~ecXZ>)Nf9YA3APr9jn+GlUZFTT3#?5>>=!ELtdew!UemhCp*gp z+ieL2hhbl1>3EXUv+Y7y)qw|nX?M^MUZEW~&lL%Ot64fu9z48m%kp*IbNXNa=YHYy z%h|PQG0h3f0ZszM4K`ZxTee=vH)zkJcDP_em*hpBj1lS+@w8TV-OB5^D^JzDO+jTL z9><=dnnh)<+~4`jCvyjLnv+yAg<5^VMd7K~s)98`d$f0`~KY)`LJh*nhbS_|pB6At?gLnbRB_E}ZAE}Qs2<&20YD~$-By%an3i{}qz8j<3? zSWA}^Xt&SnNaWKO6~2!pnAD~macIx*6rG~LYCdFfyA`$FvbTjZ4i<^9e0SYu`3C(o zwB2CpwodAGwF9qHU0j(silI%=PurV|`qXFLR^))IK90XmzAy}g#XjCnmQ`-DEuRMJ z&?ob@@s1(zwei91lDRM0bMC6#@$}V5YD{}Bi*fy%zYrzgFFZZ|M&P5g*+HY7Ei?fw z|0#PmeGPO?Tzv`a%B>4a6&VViZ8&jjfpsv95xT+svVy_DF_UR!PR4bG%ZRefL2iF(*8YU0E8 zNTX#wb^1z`^ei{IsggwE(MpdNb)H)uIM+!!PUI>#k~sh#lYc{xJr$V^tJ|zQ?Vzl$ z`5e-WjOQMzGxn+gGkEbfSr$Da1tc^P+rE}cvCxQRp;?(%#;(SZO~ljQX0d#Kcy{W^ zE&Jt2k+KV{hw5cRvZ`=75bAWY>$KQ*B{@@tHc|F?qCL0t8Lck_-oFmU-w>k67fCj; zR_jT!vWd+v=-CkPB_Eu^Fk4C&HX>Ev z0rGsOqVv1Jch9pI?3=F4$@~1(q+)S#@jFRWOn?zA;C1LB%ute5PY=A-hS-VJ(|?=C zOPUx2VSEwvbWmD$J~lofUqcqw?(If?2ovmChDn^CbW!WI`A5B_ zI9+8+c=|L>x)-G!0MUl5TDV+!l&CgG!{acA7_fQ)em6&Qt=EoDxp$+yQ|C8*jVGsE zEv8U&R1&o6kc|p^g{j#|`FW=!X9KQp^<=27HHOKE4nd=vDOUs7jW{6IQ92 z@_w0b3?I@K|0+RWaws>rCNuln5>`o%#Cs+s?&RY1MCB*f$M^Ug8ipfsH%d>=t$49t zRARfW*^1RwQ>4>F{axF>QM9B_)7q&}E$@EpOriLf?B+FZ7QdU2`2u3|U}+p7K?xT}9G zSj@&3-E(8Lkdqr}osFg7CgjDgx5)Qn^YpW1E^a>_uRL2w`^vTuHE=moT}bBel%a8Y z8c&!%bp%2|Xm z_YU4zmnif@TQ2L4c)KiMJj%;j6L_fa7Op?Q7Llq{=Z%$34oB#zl=aHyJROt_LSF?Qg z36hBU*`&ew7gv}N#LZI>*_bTk`EpH$nj&|-rr3+(wnAW^qNAg*c`1ReK>lc0O*}bB>`uZg5QtACu!| z;-^Tj$&$0V`;wk(O5M_VW$c#}AOErVlH~Oq&K(n!{+(@4yE#cq&%6ggAqfxl#itsB ze~;N-3d#=?7b)oM#_@ZmfHBI;E=gnG(=Ia+gMrrrU%9m9Ujtmnc^OJtFoqV6u))vb zw|?_D|GgZo>hEC%IY(nb#;(3lIPUX~kq%ly%M~Q6@wyi9Z^uh4kKPhpI{c<)cf?|F0`n?q#HjAMqAi{wt5_p6KO*2&d1$!nz-Hj6ya@8xYGq9Lu5BiEvMNZ&g7D4V}k z(AwUyd*9>Zc5iWzZQs%wm8cBPkFQ)`DL3Dk_`RXtucoFZ$y-!2FkEf;i1SpT!r1#@ z262Z(a8^hk%6w9vbvjEME=Q}K@KH4Gi5za-@JMJ1|C|&fmp6o!C_2#Id3j<_7JVG(i|w(hw)jD zT`~Lt&eO!cjP29RVipPOJ`@Rz-ZStK{e&>Lg=d55+Q3Y#rNpW*`Hv~*4A8XRJLPwU zO}F+TWvqHSMn&2Q)fobLpyRL1Rl~(`*HZ?qTa;7}SG6jlV>93vIjTfX1z07o zfX@0cO#w$;t7vv_DGP6Y+=mY#+xRV$I2EVsVc~`M$bI3cnfDUJ_BQCm7V0A?g9UgV zg1~IijK-SczHD)?~mTXUN)-e^ZpvoBWa+w zeupJrF6lwBhjBD@V1BgbMaQRf!R_kD=|gcjDR*Dtj9Be6!!%ow73GfA%47xEsHXCy zM}@8Bg>>9Wm*L6o)of{TwUW~Q2HPrXdraq%2=c&9%Epqj8moe1SGW6oC|VSTZ39_^ zn|m*S(>`~JEu+R|3SszKIfOE1a+b>{RRdo9cC#2&Y)wC@s!R8AksJH+R!A{4`wRVR zURMp+<@xiWJ{FmY2Q%oZ1%N58io7_*$AC!6$E)jMVvW0 zUDWg>^Pyeikk@8!CIB(A`5RqKEP8z292bIhvKQ3>4e}XIhK@1&GkWnq`cqt9o6Xzh zSq_*Ol+|@7fv|)UinEjK{O$x9o9JFOg(KarpITr@5C+?AUVtnejX2rJ?Y``;YWgR= z8D^!#GYp5;zR2!$Tb%!18U=&`?`KBg?Cj<|Hr|=?9z0Zc-{^Y(u@SlB%by5_V19f_ zAi=l2(CfM6O9S1MFHe)k7S=q(mHl4TSXEA+dTNFadhlfzfxcJIL z#!kO`q!_gXoeqClu?#1$WHk>iow2C`!2iPFm3Y zIJJqikufvF%3oS6>~#@c=BWzgmdp+KZ}99J##_hqh&WK6`>an1LT+cln!)@G=UQ)f zg~+2!XKm21y!SNQt*s08#z}ep zOEI-L)J!<5enFWLhDSbM!s$%^IS)CmOlaq+^=(=0BXyJt+`0Ujfu1Eonvtfim!i6rLI&pS^U z#tG9z6IqLuHAj`5VTn~%obrF;XnVR(sm>G7nOMS4Q3N^~3izMiM zpNRV4aBkQ3RzIo8<7ExKtiS5u$NYG$pSQ84MMZx}RAF+pu%Z_ohY#zg$1?^Lcq<1s zH5g9ERF>)5+7|Ll7c1Z-Bi-Mp%BEhoM=pdt-^Q=sy;VvRLLOcAWo2Qx3N!HKuczy-A40;)^y}6= zaSqm&AdGiD2eW74Gv(riJ~LI%@*dhrGuO>U!0xhCoGCany1XP5LBt8dZ@kEOTJv6@ zv3nYps4hgBQf==;wp2bF$tqlsFTTsC|a6 zo0_&>qi2e<%6~x8$%o`{dCfX)US!q4jT#}kf<+wMtoLz+vW#&Dh(7Zad~?07-7qo8 zSU^krb9ej;>vh5-h)0YWEwLlBW`2V{MaOZrW3i<#En3U(uJ?rr@R5^C2mbu{?se6l z&^^cFj-Zh|!-za+NnDg+4uPL`zm=r!$ATgZ6d*94DYVdOcSzos$pZTo>+x z1Xfxi#hS@fKgwnRC_(z1{G;?QnTq@#ISrYjMmCTk=g;^qO$ueM7EUDETWRd{sk@lZ z8kOBmpwHO;UUTc{{+V@smu)FDx{r?iG7YYGx;6gJ_v+8|ly=&5p*fSh3Y}n&?Ffl? zQ^NL_k7d4>pnqYk`OE1O9c9kcHWlH&U0^!)8*K)7A#${(-mJGiRH#hNxO9opyj8~o z0*yrF$qf+D0+7bfCxW=_z>|lFR;nx+&oph_vY$w~ZcH0n=S(JSYziD{iMC9Trsq>y zU|YyGELEh1@1VHx6(r4N|xY;AcWAGofK0rsNM+3=)2AbK-eda1$Q4eE zw3x><7se^r6z;sy#BKy@>lB!sgFADfVw0i3vW1lHO=a{j@3|9B)1qk>=Q#suPSpOM zMbHGp?ob>!5czBmbR9VG)$hOC;%6l#B|)+9hRJIeOK#u3{c3P%NS(FS120D_n(#~u zh|A5rmYR|h$^Sxr8*VMWxE>MBKUi*0y$#zM5CgvMu-1F_>>04o?p=6~Z`M4Lf3PzG zmNjAQ63zeOdn$BY7cHLL_||7l3_~lAP-EqMkzkX%f*U|QYa&EM^2-y6L_eO{wm43o z+6`dLU!dng#s2+CIK^DfK9S>Waom4fpIZ!=Z*vn+g9>co%f{$IKntMwM=wRnMdWY7ixKoDoh=g*(h9;OX* z|2*}}n3K#`0!$BCjm$yk739q8?v5;s@IFNgLdJ`3ZF~;3E!w->mq)87e*a^*FC!1- zRIsq4!4%^XTG5}J-2t?$zQEaK1sU~D-`(H%fUkqpia5&T{`-YZbA+2gDDrsA!h53y zNV*rnL!D{wuG4Qzagk&RnI|%yX!--fh61jqjAbQa=1n}FuRCDr1TXK?xz81+a0M)p zLVLT6pQ)ZGnS~PD^jSq>h0hi-q0-aA9GPI8!fi4>8(dTLvZ6{CvgO6^=`q-Nb#JE< zo&7YW!FKDyriad?K}vw1dR`H0v`dj4ZYxm7TQIS=*J=7v46aH8a(2pk{c)+j2XY#e zJNm7?y$`G@_Y`?7ll0uIAvx`qzTKcd0@5fYdDLe`4?GczKC_VNEN_dnG{wU7 zs*G2z!OMB$yo=0Xwn2!?oN}wLN979Ibyi-ypm4P|(N)1e#&p#dII!CC zF`3Y-urPL0_*{`YK}UNol(`5o$&k;ycN2F?oTQS}T&Ht{G9GuFrbynHKBk!s=^_%3 z-!MQL+V-;li*Pmc79Yj`qQ1U9?DJ<6WIuTcnvKhBH?~4r;w#sV5DR6^k^SuF&qJ)N zkO-66YWv~LFlR9}*5$coZ13X+O*K}TwGLEDeyEG%TCuwLK!=D=nk1=VGhOGb69-H18J>*R__ zNTh{@odL3x1=cC+N0w6bBn5#o(9_p|m2-0$+F-Ccs|=gZqGkGPu}Y3>tpkYm2tJ( zI@$f7nEi;yqTk>(2mT1Dmv>Iu3{I_mP9}QBuU^MFCrlA2nd|D%AH(`p`Tb8=5B&G& z-G8&itIy2LYOkBR%W{jo<~(!eOa=xsqSG171InJ=aZg8>Tsgbr|l3PoAOD9`| z5is)q+8>uZU4XQs+f>S)7VzRHwz~rC;5G;88fYvl*&0fV&ZLd8ERdl_;t-pD@4UJ) zXw+;RiZjB{4*5SZDfgt7%+neD$MOXsc&Y_p9sT{0+S=Mc1Q>f*_AtVG>pP8GDQ2Ix**0v4>?M12j=S+%BjqKo-4dP%#aqaxxxw4GHx~nzU}OM zkeZeT02*OxeXGh%$r`{)Dyh~?FQ~OTxu;n$D$+7~Y;3S0olrS)hzwB^v?#hi*Xtr> zTJU1D*k0@fT7f00@8$u3xn$IN#bORFR6lXUGYJ$5r8)#?^I!s*c>E{u z>t!dYVN^6q_yHV=>($SWHA9elPq50(Lu*u-_%$N*@g;XrgMqkC!&KRIh?Jj0e`&X0 z4I1Ne7qs97Kv>fw?zzlsHjCjR;QGuPV8x+6$;4ia9B`jN%pvGm%m>4bui?zz)TGDq zarrE#Yd%Y%7pDaTxy|F27y`ww;H}dFG-GX~8)D=D(ED$iU*LJ>E2{;kcF`qSH{y-& z6NLKt^0%odXk!BR=q>V1_pZR2g^;klIX`gmRn3!-8_FEa!FQVyiV@3+I+3%Xjc;+D zC6X2DChFn)AesmJJG1{f99Gz~X@ePL-pE*+P;zM&8N)?=N zr7uV;eJk~cmq&-t*;zhfskIl#c3HaJ@yw8{6X^ZR{t)_g!T~MKW3Mxt2(9D{m^P60 zF`rN)(~BPY#p~KuVvI)2_p$7s7hcs&*omKs$e6g%Xphe)BE519Flj#uL%{LwUU~@3 zDJiO>OCs&^Z1x*?x_BG}u*0sbySsM|m7#j5HF8~A9Rie8#@mNQ1*#Q+}7 zJ(R+ge|&F?KR&s2s|XIIgwK}u5WNajgxeo99@=yZNX@!GRBWd`ad6#bZ_AgVr`+0} zV37%UZ6Yr1x!)7;a*R!Ms!@8Wh2eK8iNxL@AGmyvsjk|pN?ZUTpr<1ttIL*Yu(C6~piL!< z|0;MfXJG%PKfGb~_XiZ z$t*JzLh2i5b}MA=Io-n6;jCtL$s3ybMi-n|&*+{)TE`Q)Jz~C(SybABepR{~8Wlcf5+-24eBOhfB0WPvPs^L2tNa8$X@e zyPUR5`!M7E4LOBfV}o>G6h{ukWLH}g;NsIfAmvm8RRwy&v41d~pqTsa#zDG*2(8W2 z;YG$4z3g%%bjOzms(_nm==^x-L4wF&^=Swib7i z=0;adt9DVX%+O2+64#(AjH?T~QEA2Rsw%%INShr#Ceq}nKS+lFQkP+xQ*mIPe0m<; z3j@#}cb|x0%#J6x)SyW*qA7o>UG2xvh;Q4&3;Hsw&F_w>FZQVh3o&`M=1 zrYlZVUCJv*NkBO3W6@II3R5Ro_4#NVit6c%yHRB5cRE{8cpvr-OgIqRU=jdt4+`Sr z<*n1nT@OkPbQzcx7KIe_jY@yvajAU#lkF_rNLsq`Y=rQ%DH3juwA`i~?kSVpELQ&D zZ!|EmdR_o)0zh7BLmVYN!6lMkVo83$WCTTKGt6ij>Wi|<>+_+6DN$h7$o_W`My*CU z51k2>=6jfH%50_OKgz&+ArN>;x}cuLBXbo|FAhehu>4yK3vuVlm@=%5i=RI&%8PP3 z_;aGaN&%KLc_zWD&&+zO@iI_wffTVV;;02-u5j2}HPoKLj3<8vET(wDUIaYA9zp2{ z7X!q}mtIynjRXkNE8_u?gvJG+1aL*7i2=mBKZ>Bh@xSmo0MTXT$fAyGeX2}BDx;t< zv^oATL!KuDX8pd`?pgOeBe?#cHAJexX3xhD)PIO|c1wX_U)b}7xjwOsS&6zan**J; zO~OuY#dHQu>OIUH?4>OfZ?97Ra=Bsu8PqMij{+xKzTNbfi!vfh+(1wk>>%EnAg@SpFy$0AF8L0S=&57|v^5 zHceC*r1@$CyuWonY+Pwgyy@@DAupTw>Q&;tPaf#LPj%ROoX^kb42OrMeF)48gTvEO zQUs^Z>LSUBzoOS4yty?K-Y)uu758FDC#*Z8Up5un^lb`FAJ5cFnUITtgh5lvpe;rF zcNhv>DYx!qxJqH0aPMXta50uyNXandbA>`Ho{=ntxFJaFXl9d8tGVaiE?Gygz^!{vpsM z-|a}|7`p&zwbsHX*QrQaYqIN31`Y7ve1xFcP7|UFrBV|UZ!Iq`rv*s7Sc)tvrlSCG z^!&LF04T$gcs4W8_h2N51~FMMVr!;O!0QN<*eUNo9gd z_{u{vD_DIKTMRb{apg<#J*%(m#BGjc5Iz}^ zi(C-Tyo<=kKe(1TY;CPgQetHAyE^ZFD9PWcUnEZR2CA_JIY@C#e! z0y<967(JG~qe}e+fEi|&rE|KYt#E$hnQMz5%t9(8dyqiyjYqs4>)O6*a#t0AQ&*W( z^yc-_;#)isQ0a3$Ilmn3Bw*g2aKk?fU5eCC?=YiWpeUV>=Lh#wpQsU#iBlz6RYMr2 z1V%IKXFYB>xc!+|(Vy8`C(png8eq@QyI)3lc?@uZ@UFfdgiE0DAO!JwIxx)Tfv#C2 zpZ2tHBQ_P{V%^o+yYGspPe}tC!;6z{8)E!{5}z*zM=L?ipPBdq@kGJMl5v0V?jGZ2 z=FHEByQ=2~*o!ucyEnz#?t`EMYS+bn(W&4ZRh&EXN^{dLywHkIPsq z{f(iOWA9wI3Tm1Cj{qg=&{V1n2LPb%8(Lw|cX*rg!@O;S%0i-W2Tbf%gZ^rj-d>6n zd1k?@Ub++dNFE+$R`!}@F)sHSMp8yDtvOe3)-M7PzkMm9Sb0p`@Q1C8xBNW_h-TJv z=0m#X!9-Igeq)K1b|1lv_$vZB9-eV;?FX}H23|AAK0vj!5;r$rfE8tYs3kiyZJvOB zWNK=va{vA{$WkVD&BNQh@d8-Ub2;lG<-sf?5FUP!++HE$v=4gE(gJ$dsslZ?czkSr zM1<5!R~D1-y3tm7J89`Rwz}ie0+`A*iyo5rm%o$s9cB&FqL<#Y`5>S>TW=927mYlK zsf_s>#5zWG*8kTOi2$!TztDdzvm!1w~bzPeVA3QpZa>KBYfY4)&LU{#vmSg z2>xljW-cz@m;9>c(5LA-b6ep}zST$_FEHOP!BFkvR{;D~rfFmuP`k2K;DW{}nSps1 zR5d>FyWZas+U+K@E84%T*pD5+#@Q82ObZKgDZv&TJsppnqr&X&k6m&2cD! zjO1_x1N{o8Avq2AvS`Qm|}jBuifbzPFlI*KV5z^ zv0806H`XC52GS?apHC9nkCBr=P>U6AKPVQfRKK**Vb1NYwqK9fg%U5NUZoTX!T+&m}|>xwhtkvk~kJ;8jG* zM5ixR9fpq>N!THv#bj2Xx*0lI_MwM zfTi$D3hd+C^B|u`Fy~ne;8DLsrQR_&Ol5`HC&U-bX75Q_#yZ2_W!+y?e$j10QI5(2 z0>E*t0$TfXD6zv;KwVr9)(z&M&TD|{-i}-`$n=E2So0DpSr-OUb1fsbM*!vpNERnW zFqAGoJqqQ!AK-_$^c|k_Jnln3)=Sd~2fYVltbah?bl^9D(Z|yj(6PDOX30fyxtw>fL|VRB}~S zPw$zEN|UzTlhkOIXxqJ+IPd_-W24Y?NONF;dwb5=tYaBo%`a@nw6niPD{_qH>nXY` zB0cMD_CvQ6ACRG*h}$7_7MDzH@vkV@gq5(X;BYiB)I*Z_Kv)sQ8>nQq4qHOh9VGLBhzR^tMAan}xN1-DcvM z(-~>q&1c8a0+;@l&jbNnJ3AR<<&`b@C>4zW*SI?2==Y)s7~`NSqh@uQ9t;Gx5~#ioLw+46pLQO!MCQ11Evw%!b8QkcXop zw5KCQ_EGCy2L%AYtTx_P2@t$y9;Ljyy+eM=r%#_k@gcI@A?$T1b2u+6!p=K1)7H=Y zynM#%*9QxWi(~i)*Crc?lCGlwZDZ%;1goe702r1Q)VJ8IdNzuG(AxfdXNa`1=-=wT z`M&^cFq=_N4Sm)0XZe@+rXGuTahFagQBp3oDq-cuz9##Ot-bHH4E{RKOtWHD6+>u{ z1nSajT__R5$W*k5ktMRR37^#)V}Bk{>}yrgmF~-ZL|n%je>_mEeg+f))2iozx*0$> z=WtQ~R;$3}L3jS~iS%YP#AY#8`VXH?7y$ zu#b_JFfWU{pi%l~gu}>Zk%jJEO0*g1ePp8+v~nF@4xk$p(ge^yJQuHINmGWo!KFj# zGp*gR_6&gZt7)`=!;i_D@Vjn*k6m|}rn;v^+c+=Rcr4Bh0qTi$C1tjZKmdvF#+a9P zue+{1607zvFf$gRV|4M^r$VSR9R`gRgDF~dk zu8MO7WRojKTd79mNzR{|afEP7S09_m4V>9DNq4ceICz&wr9#IlHWw*Yb8f8dfcjl> zq57|X)=IP6TG7A{sLbUK)2nK^Xe9xFG{KD1^&C}ZN3%QV-c3fQ_U-1Yl9cRe!Og~E zwFRHN+pWIlake>n#1L22zB5RNSN|9-z~t@(ALKkl;LZTgH=VNeNqG>&0Bq}UeW}8UwM}6==!sdT*d(+=e{7*Zb3QY0A^J6A2Z0391E~Te}9Ul%oG~;K_f(dmb?Jf5X_@_GRWwr zDXCEj-S*)}hr#5J4g;XupeDBouoB^_4o|)6Cu}l?Qa0y7+1^hV|ISvtcEauFD`Fpk z6}Lz5+b-=Sdc>}9@p_|`zAWKw*JiQH_P5SdKbdVi!wkdwhs^h3-3NX^pE3fy2?r}( z(Fdz5NqHTgSZ%ojv^V^q{J?3sei-L2{r}PTtfXUs>hW&mP*(@E6q_CU;(2Fhc~hiiJBUV~ z&oZWU0Yy&f6+R)MR-N4Z@7OV0rN5`t?J%L(QM-*RK+63eYo(XCUI1i%_NadxU_`PT>l){GIxhkk94Lxu zTVjnW3fpu-PckPsr8$(iE|OddEU-T~D*O{HbtbE{bN|aN+?680U%pS5#Fr9)=M=HO z{!u&MZ4dib?SOzX6s5aQ$o>y$eE@#aREW00{Jo;uK1L9ViwLzwT4LQN*gT%i3Io(d z=HkU@csS6LMAm+iB&_cih4{ims{s4MHs1YBa12W6@WHrQ$005++Iy(@f<~Sp`*XI< z0$tEu!trPZ*U_iQMvr12V`b0Hm>)@noeZ@ajphUx&);li9w&*Wl~tB`0VlzDPYJnD z696UzTmt@0@M1n-xs1XUS_6!1G5q=tlINcIFcz0c8_URu%&c zXaL=&5tFqpLT3aBSBnVDRw>sM4_(XAG`v15rtkG64e=dpY@9ID7H7>L%ZzcVUkME| zietw(0W4~_TjNblTbwW#w>!~Q+RYdaL@w@wnnO=v#;HB+KD|1fNBuSb;(8J6(0>y! zUkx_qkP4Z!BSQ&5V`FWzBMmKLj0^;Y0*xmxq-1jz?*quTVq@XUVsC;3m(L^_(Oxdk zw??4I$SiXiZt!4%5nXcFg8FX*EkFREB`4f8?lay2DnOYqWnZNuM}Fd5O;4=Xpu^TH zLr`}vX;cgVxy`vv*nU+hSziFx9+izthW_tpecbO0`2V2MGVI3n>n2gTyF{ak=~u%* z@7m-bv}pA24Og@Fd=Fqg4iPE=m3%5Z4Llo(geEKsie< zl6|a{yWTcr@6Tfq=d7`HP4_d1_RM?Iz*?Pk`CCWM&hixyAkbB;Z^|Uew_b-VJ#rgh zo2EGh1IE8*Pycdp+XwI>_w6{>=WUt&B>qM8j($S47?JdqjYQQ^1{6bxO~vVjP+MKD zjfEyOrPVSI42bqL=Mo+Dx)m5 z#04nzfSUmSxB~GR=%kP~*Vr`SHS&PFj2K%7+xsEDoR{&?k>gwecpu;va@#|$mYdan zyDe8^EE%NjTzZm1#YR(ks#4>u#`z2VPnuT_i9`WGWt!-!v5P?+|5rQ|8l?>SUK^k48Ml78adCQy5~vGKY%SiNW`8`gjt< z!$fgrXnJ$L$96IH+7Fv(E8V(oxH{Vxd7OvszrO!!B5LY!KG50{1`kU~VHAGgGhtrH z967s(eI9dFPegr(OS4Y1B9cG`sY4tE-pQKJ;^6uat+d`Jzo_Fb|GT@`%OQ?>=u)n) zQ&{;B`1P(eDVU$k+=E!CvdiD4L%W|{j(PN-R_$XvTKIo%b6^*)|9+$#=dIP$x*5%S z#f;z85qLILa=hs;>E;b^oZtVQTKq1D_}!P|n4H_^i@bY>jlKVm)~`2j@&FPNu5)$! znCcK(Z1F??Mr0EuHC1G9->q^)~zN|TzV0sb~RWWV?gy8<{q5Q+JTpBV^(4*|wn6~SV4 zqo7v5V#Dgi4qV!E(*|hwky$;$&|o6d4wXHR$}X|YW>+toOppOwE0`ycY7iI4wT%+V z-U;*)i~eAwsQSjJP9b5&EM+kOWN3!8v}aHz;?eX@0MLmv5XrOYi?5x{qxkjloHZO? z+vk5~m*(}n=Rns8&`9vz0Xv$np`>~3)77y<%FGo&>~bF7SXh#pgA7(T1F_|1_Y|`m zY}b`nKREo`_5VkDd$IjbphOa-YjXMF$K+TBZK!>So@4dPvP~CNsJTGQzRdVl3b3o! znw8dh;#9Lxjs}bCiluNy=U-8B2@oOLa!4Qbw6(##lxY zGH7f=c9o^&pki{+kV4I3#?lzOLSe*UOk_=rVT>h%!C3B>bMNnQ|Gtm=xWD`Vclmt3 z-}h(vzTdCc^CP;Fl7Y(s>d!i}sf1r!!q_Q^^TZ{}VUCB#@$m;&Qk+YX`L{zBj{~K@ zdtud;@|-(tVR?(yRsRncTZD-B{Dq>nBINvuncufsTrXY)Y^s*X3ebqUT4ts(-QFdO z>RkB8Q6b%rHlOl<7gA|^Atd#CLY()d)CcPY3i>v`o39-fdw_Q~7up_EgCJBkxUL`0s$xR=5oVM=+Y zXG3dfQ8rK61^EmKvet){2?a8EIBd+_brs$m91eFM>rRPu`-mPZbdCqZS)rIQJDlf| z>JK}){X8hk$2fa9q#PFjrqRMygdfc_v<%+NN=KnM<3U^q@kUJ3i!6-a)N(v87 zID6v2PUS8^{RHOpK(Emkzy2~VN!xie#^QVPiY;jfTP$^W3^&moa&qY8&ql({dq10? zmZ>moF8Ni>#7X@xiXjm-Y@nhF!#V{m{=kPwVQwlNL>D{(uY&pvbYnktW*(QCbjXD# z_Xe#9odf&B4aCXmCFHY5x+p<*GKojs0}SC7r>Zx@?eNt*9cResWW@&WIA zm)Dm)_%LC{^RR8s-DJ7%NCpF$J*y+dJJ|Y6xExJXsOVf@eiWyob7STXJ1kgfKG=Ffue0}iZsv?ha8b?nmlpfR&vC&Q z7%f)NZ*pGjdnYSFx9!f0SP|K)UbXQnYDpdP_c}%157V8SQr7RoX)8X7A=2$*Y|$Pi z{&nuKKC!gQ@~9AdDJ>iElOnN%@d=uoC=eYI4M=4Vvo0X5D&ANr2bi$g=8?BzQ1hzN z=Dy_9M-p9|%F% z@{qmP$8aKF&dHy(1L$T$?i|9^(gGk7;RijT-@dX-XOQmejs1Mz1Dy8F{H})EN&X@a8xoEd#uGBrn9 zRRkWCQ7luyY>Y{dfH8?mx8oq z>10UHH%vWOe(gb}6q)QWx7k*&Wr*i*|1@Li1ZNkuMHZ{wq$!;uK}>G5QL>?lNlJ=f zcwKs1lkh=DlL&&z=g3s8h0FX6sj43AVXny~7=CVTC$S{sYwsruwDXukWUhk(Iq^Ng zw@>@@wg>xPI#j%E)sH9VVV@_hW*Ip*mSunAaNh#&S`r+wSkWjWIGi0?&Z(d_n-m(( z(_yj9qsN4@2EZUx@7Hf!3*o+6c+xfc*LkDAj0$@}&T3j=b!QOZdBa%h%MPV1n7Io? z&v*6=GM?t?IU@BWBG=}KQld6W5I0?)k;>1FBn3{~B@J>SB$-mBfzbB>vazTz$M(}S zzu)|z>YrwAFI4NTD*YOYfMiFe_4FSGCYC8!PLe$!fE*dI{?+}67Uhu-b6dYQlWFf2 zyy*T&H!u&SJWh{uT!G)rdG|>I*fJPQrGI^3OA7K>Z`v)RVVQUl+L|P~`q3)OHA;q0 zUv_>MFl1vH*P8Vu3Y72VP>6HQGVJE&Nvg|h%nZ1rJ;E`pnqHz4S0<}h)O8~*qp$qg z!|D60Obng)t-EU0;j_@2Ufvg)X?2qSst%Pf=S{atb+->uwC`IbbR2x(d%*9_XNj+A zqqhaQ;v^%bU0ft5Un%I=;RBgpCRWZ3t#(%(3gh|SBR7GhCY}5I!WE9GNw2FtxOwjZ zYilMryUSzDn4)BHXjUv=vu1nx_%R5|_%EnZB|tT){suxR>|Ck6|1~^}Pm`cqSFQeq z%|l6D$kQ&&5(2~)bZgNqFA=J^WmnKxyRDW+pb9$6OI$%HD`7KnreS_G8b@g}PL*7c zzlbWT8`8|HEym2u**=BcjVd}8sC_smo&R;(hi{|${S&MN7eE`Fjei)FI04l&+}b*4 zzs8P*XRez2x@5y9N`!xIt!p(NXWP}#02Tr^Ju~f~$-+C?-OMd9b&nK>Jdg4#7884= zL|Km=6E2=K(|<6er=}J(wFJ^SF?THQgWt+ZfjYb^VxjUrP&vWNLNCWWeCG$fs`h4h zdbSpN^vqv_`H7$2>IrtRYr&wFZWr z1+~dG(9B4@rI{yb%7+0IZGn9yU6&>ja zd>3fY6aF<2k~Co;La)h+emw5t-4Hl4Z>SzWpi&rz>c?QsSU1@NP#;<;CzqFlI1$@w zEy-7Hylr3It3X~=e>ZSq?`{o)T9y(pBFD9-;ZR17jD82V)#% zO((PUg`q2`P)e-o&Yv&m^!|+N>J;5f zoLGQm3~Bwcsitd|ptHCULQB=DD(exH_*ALqtjL^V(^l5RD5TB?e-=%&{UZLDbKzc! zs$tWLKlN1S_DOWJFxuu=^$yvm!jk<0vC%aC*D4<7%-O&!ZZRY4;Cg0!^u1$hN`HnQ z{EpG@gL*Gb%DyK(%_qiJIG&Z~&;6kx?$8%mEEB)uk@5tFM6;qU*r#R7X3xl=t63vG zrbStoyY^?{j3YritK%UpYh&NPbu`MX)T;ibLnCGlJsHxh8gOA#1jz40HeYW$gpdr& zf>l220ZT5@yQLj|BVTN?UAtqn79fvf>R2uD=EJY@5bZnShT6Ihk;Y9&x7!T~wyCoG zu4;rQ*MS1mNBl|U#+@3iq`Y541HCcRZr6UiP)s$opu}G_JL{uEFT%O{@XpL%?Q&8K z?YCY_++TM5nEx)xtgN1Z)4kO8cn5M~rY_0tp=6CJghfi`3p=-IM79A7&T7_aUDB!F ze!Ws6p_7oYF}Jv%E)T_Y&)frCh+Fg;f&c%P@`O(gB+6)XQ49J9*m%GF3w7`oKea%z za|R-3c_cc0p_AyFh&(%xDB<*zia@zi74Gh+ng=%)m`v-wSRXKO%g5L@l>E_taLZ2D zg6l^I_UfbhUuR!@>J+-70H09znWbpnSa=-3iqN;eevLZUOS(}u@Qz<+Ezws1PiSDU zJD@r>4osIMEa}TM^s_GY_ZBIllldC{(4frQm{34EedC@}B682E8ShvL45fX>x`Gz< zlu4FC|M0z*CNO$<$@0_9YXB&!&&Btg*mxS_$XBuGg9On)IA;K#N1W>K`OryLVy?U{ zfN*-M==K)XsxdQv9yk{+QQbi(QT9Pkj>~jqKJ`yAJ)vV};WFa8`Ty8kXa+Oy&9-j- zDtniOB3!6$v8th7Q64mw8<8?Y9vhs;@=no4M+htJaG3MttorKX=u%Sz%+&tFxWDQ4 zVbPFfl?26zm6P43 zSbiCT(oD(ysHaS}-V zds%s?;$!@qZ@-N$-8d*6=fIw&w+lwgSU|`fJs`fj!OXa4?rH|nKNJ@H)W*#w!|Rdx z_|P-K$Z6A}p2;GFJcD{n-`rxzenLKj-J=p3X>f5#^Qebb6q2%}G6rpGiL6#lOm@EZ zhR?O{oC_$fJrGH;#mQi;n&C}n*=JCfE^QkAqw9sY65wR=wRO4Qa40UUpb9x}$}cdo z;m8=0mea2mA9`@Y8$SLP#rD~372hV(aRTZ_Pc8Fjz80;?&HC^WYr(&RkT3RRCY42A zsWRa}v7(J6lVa_G^_}TxITdopXx9~57~pkVY5Q~lF34X3g`RfGRRkPwsd?E|Qp9x^ zQu*X68gZxQuu|rpzXLWzSb7bg+yS8)Ay3&{p>d88Wqpn#+&ZEzFCJZQd5&*4o$O{` zQFz{mYDrNm;kr`7-=pr&NOkSz+{D8zG>d2LR8hIsmihF1 zrxE^joQzC!a+>F8>6fejl|%XIP~hN$N6yOmmL1*_p7P(27(jJGTkpSeE~PvEC9wSW znX~_GuJHe}8{fhJR-z+EH9b3CWMA=zdrq$A^UU2#%!kH{W(iP0gXDm zkw3cXrOH3Gp-tKyw#ndoq}nVYSE}QSMsok*w;?pUoC&Sxi=ApcVNQPd(1&?jfunzJ z+5DJ4FG3yzs?SXO$Tk3DR>do4YE1T!JHqVsy$#qrd*sX99`>eZZl-!O%G6pzQUv%( zV)>uEkOGucd<&0|hcoIXGiv4RQ{3`$OMmR;EH`(Cx0#*A-%L`N2H8di{kb@Cx<~*E zLuqg?VrN^a6Wj2fT;KI~Z@hrqCT4omR^!xkq%h&;k@#RbL& zN3Xu?d#{`bwlMdsasths@s0JDK{VG;P*v{9nk@oSrC;(&woBqFHi0wCd}_vVOeJFS zQ}0sXb>_Js$25;`LSm#-wnAJ4fQ5VAGd;o(gpW;ExEGhV><3!=F>teMJ=@)(SDd08 z%8zj4Wt7ib^uL+9odr_$y3Ss{B{z7DIGeA7F4eFxY0IAybFbSOs!F`^nX(IK(VqQM z^!szF2r$H*b=00D+Ri6Gf2@7D*G+TgVM$|>N?ANljgti_DTod3Mg73*B*#3ySwQWjrHNtQ?XTuFDTQ#&{ z9@@KSGeq`+;YXA*Aq(Z=lhcFwv!C5#pBEb$&aEGkQcmb>7w_yD0O{z!$Fc2?eMZyw z;&czdho!?Zhh=-^h_~lUCtn2IsHQaTQ>CtGK^%3ITIfaBhSx_1whgqc_jZbGb2dho zJn)8?Kf1oHm!zv_hX3&+x;$7PU{17vD*SN!`_asW-u$7pMnQYrz7HpMp^7dAleN*D#CoZHrGFt)JYfLM?qgx!o3_0La4 zxb^f^U&;tt@Tynn$ez-jygNWSH!yx|vE^AqWaxCHb$ZjH9d+4R%3QY*$XGIpxF7CS zUrC;d7rrRgN`JS1+5DJeyIKD$25^gy7ZWyTQ9s@HQ_~a}r}%|1&sNSRZGPcB_cLnT zD(eZ{o#Fb8*BzZ6L`%N~N}H8m%#~M!VeXm**M4-BA_7kNTCG_y`|)HtduDENGLF3% z2p(Hk{pg)k#3p5m4d3+$COafBOVTzZ4~zFUL4s|G{cQF>FU1mn+I_80iP;m^Z}DC3;v2?31mjZzXoAJf)KgxJ(0`1Iygu*ntm5y>KA;z+tVMwluG7) z3$yyLuoeoS3g6NYOYCQSC}iX*(8A~>xqpVE24)6Kc(b3tiC0!=UC=DR(nIZ=aRKQW z)TJq}>nxuoi4p|b$L}$N9>fUn!J91Hh{GJ{qEStYhAf+mK~Z^Nsli|9v0qlWeN-5m zvS(?^^Lp3fpq{Z!MkWB)Wvk4u6li~N!kLW1-ea#k{{AB{98~t!&Y0Rs(4O-k^~&1g z_|W-cYYE3whVvV0f8?Wyy@=O;Gar3L)5y1M$ygU!RnOTTSG*&t?R@XKA( z2v~5ufsX#f#?u(dZ)gcORsgdhv_STj>A4HWEGi#d)Dt&@iHf2Q!fF!>c^{}fD2txTENw9#&a}SY*HUoLr3`l z)|PAKMX5_S!ozSN%*+!#PvF2Kd;evU`Ts%v`XB0}{x?tSdcGD_K4mr$bpG`_ -in order to obtain sample data points to visualize. +DFF tutorials (`1 <../tutorials/tutorials.stats.1_extractor_functions.py>`_, `2 <../tutorials/tutorials.stats.2_pipeline_integration.py>`_) +showcase all the steps needed to achieve that. We will run +a special script in order to obtain richly-annotated sample data points to visualize. .. code-block:: shell - export DISABLE_INTERACTIVE_MODE=1 && python tutorials/stats/1_extractor_functions.py + python utils/stats/sample_data_provider.py Displaying the data ~~~~~~~~~~~~~~~~~~~ @@ -84,7 +89,7 @@ Using Superset | In order to view the imported dashboard, log into `Superset `_ using your username and password (which are both `superset` by default and can be configured via `.env_file`). | The dashboard will then be available in the **Dashboards** section of the Superset UI under the name of **DFF stats**. -| The dashboard has four sections, each one of them containing different kind of data. +| The dashboard is split into four sections based on the types of charts and on the chart topic. * The **Overview** section summarizes the information about user interaction with your script. And displays a weighted graph of transitions from one node to another. The data is also shown in the form of a table for better introspection capabilities. @@ -92,17 +97,11 @@ Using Superset Overview plots. -* The data displayed in the **General stats** section reports, how frequent each of the nodes in your script was visited by users. The information is aggregated in several forms for better interpretability. +* The data displayed in the **Node stats** section reports, how frequent each of the nodes in your script was visited by users. The information is aggregated in several forms for better interpretability. .. figure:: ../_static/images/general_stats.png - General stats plots. - -* The **Additional stats** section includes charts for node visit counts aggregated over various specific variables. - -.. figure:: ../_static/images/additional_stats.png - - Additional stats plots. + Node stats plots. * General service load data aggregated over time can be found in the **Service stats** section. @@ -110,9 +109,83 @@ Using Superset Service stats plots. +* The **Annotations** section contains example charts that show how annotations from supplemental pipeline services can be viewed and analyzed. + +.. figure:: ../_static/images/annotations.png + + Plots for pipeline-produced dialog annotations. + On some occasions, Superset can show warnings about the database connection being faulty. In that case, you can navigate to the `Database Connections` section through the `Settings` menu and edit the `dff_database` instance updating the credentials. .. figure:: ../_static/images/databases.png - Locate the database settings in the right corner of the screen. \ No newline at end of file + Locate the database settings in the right corner of the screen. + +Customizing the dashboard +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The most notable advantage of using Superset as a visualization tool is that it provides +an easy and intuitive way to create your own charts and to customize the dashboard. + +**Datasets** + +If you aim to create your own chart, Superset will prompt you to select a dataset to draw data from. +The current configuration provides three datasets `dff-node-stats`, `dff-stats`, and `dff-final-nodes`. +However, in most cases, you would use `dff-stats` or `dff-node-stats`. The former contains all data points, +while the latter only includes the logs produced by `get_current_label` extractor +(`see the API reference <../apiref/dff.stats.default_extractors.html#dff.stats.default_extractors.get_current_label>`_). +`dff-final-nodes` contains the same information as the said datasources, +but only aggregates the labels of nodes visited at the end of dialog graph traversal, +i.e. nodes that terminate the dialog. + +`dff-nodes-stats` uses the following variables to store the data: + +* The `context_id` field can be used to distinguish dialog contexts from each other and serves as a user identifier. +* `request_id` is the number of the dialog turn at which the data record was emitted. The data points can be aggregated over this field, showing the distribution of a variable over the dialog history. +* The `data_key` field contains the name of the extractor function that emitted the given record. Since in most cases you will only need the output of one extractor, you can filter out all the other records using filters. +* Finally, the `data` field is a set of JSON-encoded key-value pairs. The keys and values differ depending on the extractor function that emitted the data (you can essentially save arbitrary data under arbitrary keys), which makes filtering the data rows by their `data_key` all the more important. The JSON format implies that individual values need to be extracted using the Superset SQL functions (see below). + + +.. code-block:: + + JSON_VALUE(data, '$.key') + JSON_VALUE(data, '$.outer_key.nested_key') + +**Chart creation** + +.. note:: + + Chart creation is described in detail in the official Superset documentation. + We suggest that you consult it in addition to this section: + `link `_. + +Creating your own chart is as easy as navigating to the `Charts` section of the Superset app +and pressing the `Create` button. + +Initially, you will be prompted for the dataset that you want to use as well as for the chart type. +The Superset GUI provides comprehensive previews of each chart type making it very easy +to find the exact kind that you need. + +At the next step, you will be redirected to the chart creation interface. +Depending on the kind of chat that you have chosen previously, menus will be available +to choose a column for the x-axis and, optionally, a column for the y-axis. As mentioned above, +a separate menu for data filters will also be available. If you need to use the data +from the `data` column, you will need to find the `custom_sql` option when adding the column +and put in the extraction expression, as shown in the examples above. + +**Exporting the chart configuration** + +The configuration of a Superset dashboard can be easily exported and then reused +in other Superset instances. This can be done using the GUI: navigate to the +`Dashboards` section of the Superset application, locate your dashboard (named `DFF statistics` per default). +Then press the `export` button on the right and save the zip file to any convenient location. + +**Importing existing configuration files** + +If you need to restore your dashboard or update the configuration, you can import a configuration archive +that has been saved in the manner described below. + +Log in to Superset, open the `Dashboards` tab and press the import button on the right of the screen. +You will be prompted for the database password. If the database credentials match, +the updated dashboard will appear in the dashboard list. \ No newline at end of file diff --git a/makefile b/makefile index e182bfcdf..cd4a36789 100644 --- a/makefile +++ b/makefile @@ -50,7 +50,7 @@ lint: venv .PHONY: lint docker_up: - docker-compose --profile context_storage --profile stats up -d + docker-compose --profile context_storage --profile stats up -d --build .PHONY: docker_up wait_db: docker_up diff --git a/tests/stats/test_defaults.py b/tests/stats/test_defaults.py index 11c525723..d565efcdd 100644 --- a/tests/stats/test_defaults.py +++ b/tests/stats/test_defaults.py @@ -6,6 +6,7 @@ except ImportError: pytest.skip(allow_module_level=True, reason="One of the Opentelemetry packages is missing.") +import importlib from dff.script import Context from dff.pipeline.types import ExtraHandlerRuntimeInfo, ServiceRuntimeInfo from dff.stats import default_extractors @@ -20,7 +21,15 @@ ], ) async def test_get_current_label(context: Context, expected: set): - result = await default_extractors.get_current_label(context, None, {"component": {"path": "."}}) + example_module = importlib.import_module("tutorials.stats.1_extractor_functions") + runtime_info = ExtraHandlerRuntimeInfo( + func=lambda x: x, + stage="BEFORE", + component=ServiceRuntimeInfo( + path=".", name=".", timeout=None, asynchronous=False, execution_state={".": "FINISHED"} + ), + ) + result = await default_extractors.get_current_label(context, example_module.pipeline, runtime_info) assert expected.intersection(set(result.items())) == expected @@ -35,6 +44,7 @@ async def test_get_current_label(context: Context, expected: set): async def test_otlp_integration(context, expected, tracer_exporter_and_provider, log_exporter_and_provider): _, tracer_provider = tracer_exporter_and_provider log_exporter, logger_provider = log_exporter_and_provider + example_module = importlib.import_module("tutorials.stats.1_extractor_functions") instrumentor = OtelInstrumentor() if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @@ -46,7 +56,7 @@ async def test_otlp_integration(context, expected, tracer_exporter_and_provider, path=".", name=".", timeout=None, asynchronous=False, execution_state={".": "FINISHED"} ), ) - _ = await default_extractors.get_current_label(context, None, runtime_info) + _ = await default_extractors.get_current_label(context, example_module.pipeline, runtime_info) tracer_provider.force_flush() logger_provider.force_flush() assert len(log_exporter.get_finished_logs()) > 0 diff --git a/tests/stats/test_main.py b/tests/stats/test_main.py index ac72fbf9b..c6539343e 100644 --- a/tests/stats/test_main.py +++ b/tests/stats/test_main.py @@ -52,14 +52,19 @@ def dashboard_display_test(args: Namespace, session, headers, base_url: str): dashboard_res = session.get(dashboard_url, headers=headers) assert dashboard_res.status_code == 200 dashboard_json = dashboard_res.json() + print(dashboard_json["result"]["charts"]) assert sorted(dashboard_json["result"]["charts"]) == [ + "Current topic [time series bar chart]", + "Current topic slot [bar chart]", "Flow visit ratio monitor", "Node Visits", "Node counts", "Node visit ratio monitor", - "Node visits [cloud]", "Node visits [ratio]", "Node visits [sunburst]", + "Rating slot [line chart]", + "Requests", + "Responses", "Service load [users]", "Table", "Terminal labels", @@ -68,20 +73,21 @@ def dashboard_display_test(args: Namespace, session, headers, base_url: str): "Transition ratio [chord]", ] assert dashboard_json["result"]["url"] == "/superset/dashboard/dff-stats/" - assert dashboard_json["result"]["dashboard_title"] == "DFF Stats" + assert dashboard_json["result"]["dashboard_title"] == "DFF statistics dashboard" datasets_result = session.get(datasets_url, headers=headers) datasets_json = datasets_result.json() - assert datasets_json["count"] == 2 - assert datasets_json["ids"] == [1, 2] - assert [item["id"] for item in datasets_json["result"]] == [1, 2] + assert datasets_json["count"] == 3 + assert datasets_json["ids"] == [1, 2, 3] + assert [item["id"] for item in datasets_json["result"]] == [1, 2, 3] assert sorted([item["table_name"] for item in datasets_json["result"]]) == [ "dff_final_nodes", "dff_node_stats", + "dff_stats", ] charts_result = session.get(charts_url, headers=headers) charts_json = charts_result.json() - assert charts_json["count"] == 14 - assert sorted(charts_json["ids"]) == list(range(1, 15)) + assert charts_json["count"] == 17 + assert sorted(charts_json["ids"]) == list(range(1, 18)) session.close() diff --git a/tutorials/stats/1_extractor_functions.py b/tutorials/stats/1_extractor_functions.py index 20f371ba6..f8ba327c0 100644 --- a/tutorials/stats/1_extractor_functions.py +++ b/tutorials/stats/1_extractor_functions.py @@ -7,12 +7,16 @@ Statistics are collected from pipeline services by extractor functions that report the state of one or more pipeline components. The `stats` module provides several default extractors, but users are free to define their own -extractor functions. +extractor functions. You can find API reference for default extractors +[here](%doclink(api,stats.default_extractors)). -It is required that the extractors have the following uniform signature: +It is a preferred practice to define extractors as asynchronous functions. +Extractors need to have the following uniform signature: the expected arguments are always `Context`, `Pipeline`, and `ExtraHandlerRuntimeInfo`, -while the expected return value is an arbitrary `dict` or a `None`. It is a preferred practice -to define extractors as asynchronous functions. +while the expected return value is an arbitrary `dict` or a `None`. +The returned value gets persisted to Clickhouse as JSON +which is why it can contain arbitrarily nested dictionaries, +but it cannot contain any complex objects that cannot be trivially serialized. The output of these functions will be captured by an OpenTelemetry instrumentor and directed to the Opentelemetry collector server which in its turn batches and persists data @@ -49,8 +53,9 @@ an appropriate Opentelemetry exporter instance and bind it to provider classes. * Nextly, the `OtelInstrumentor` class should be constructed to log the output -of extractor functions. Custom extractors can be decorated with the `OtelInstrumentor` instance. -Default extractors are instrumented by calling the `instrument` method. +of extractor functions. Custom extractors need to be decorated +with the `OtelInstrumentor` instance. Default extractors are instrumented +by calling the `instrument` method. * The entirety of the process is illustrated in the example below. diff --git a/tutorials/stats/2_pipeline_integration.py b/tutorials/stats/2_pipeline_integration.py index de81a3593..53c3b4b3a 100644 --- a/tutorials/stats/2_pipeline_integration.py +++ b/tutorials/stats/2_pipeline_integration.py @@ -72,6 +72,10 @@ async def heavy_service(ctx: Context): the pre-service and post-service states of the context to measure the performance of various components, etc. +Some extractors, like `get_current_label`, have restrictions in terms of their run stage: +for instance, `get_current_label` needs to only be used as an `after_handler` +to function correctly. + """ # %% pipeline = Pipeline.from_dict( @@ -90,11 +94,13 @@ async def heavy_service(ctx: Context): ), Service( handler=ACTOR, - before_handler=[default_extractors.get_timing_before], + before_handler=[ + default_extractors.get_timing_before, + ], after_handler=[ get_service_state, - default_extractors.get_timing_after, default_extractors.get_current_label, + default_extractors.get_timing_after, ], ), ], diff --git a/utils/stats/sample_data_provider.py b/utils/stats/sample_data_provider.py new file mode 100644 index 000000000..d778c922a --- /dev/null +++ b/utils/stats/sample_data_provider.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# %% [markdown] +""" +This script demonstrates various instrumentation capabilities. +It also provides data for the dashboard emulating simultaneous queries +to the service by multiple users. + +""" + +# %% +import random +import asyncio +from tqdm import tqdm +from dff.script import Context, Message +from dff.pipeline import Pipeline, Service, ACTOR, ExtraHandlerRuntimeInfo +from dff.stats import ( + default_extractors, + OtelInstrumentor, +) +from dff.utils.testing.toy_script import MULTIFLOW_SCRIPT, MULTIFLOW_REQUEST_OPTIONS + +# %% +# instrumentation code +dff_instrumentor = OtelInstrumentor.from_url("grpc://localhost:4317", insecure=True) +dff_instrumentor.instrument() + + +def slot_processor_1(ctx: Context): + ctx.misc["slots"] = {**ctx.misc.get("slots", {}), "rating": random.randint(1, 10)} + + +def slot_processor_2(ctx: Context): + ctx.misc["slots"] = { + **ctx.misc.get("slots", {}), + "current_topic": random.choice(["films", "games", "books", "smalltalk"]), + } + + +@dff_instrumentor +async def get_slots(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + return ctx.misc["slots"] + + +def confidence_processor(ctx: Context): + ctx.misc["response_confidence"] = random.random() + + +@dff_instrumentor +async def get_confidence(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + data = {"response_confidence": ctx.misc["response_confidence"]} + return data + + +# %% +pipeline = Pipeline.from_dict( + { + "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]), + ], + } +) + + +# %% +async def worker(queue: asyncio.Queue): + """ + Worker function for dispatching one client message. + The client message is chosen randomly from a predetermined set of options. + It simulates pauses in between messages by calling the sleep function. + + The function also starts a new dialog as a new user, if the current dialog + ended in the fallback_node. + + :param queue: Queue for sharing context variables. + """ + ctx: Context = await queue.get() + label = ctx.last_label if ctx.last_label else pipeline.actor.fallback_label + flow, node = label[:2] + if [flow, node] == ["root", "fallback"]: + await asyncio.sleep(random.random() * 3) + ctx = Context() + flow, node = ["root", "start"] + answers = list(MULTIFLOW_REQUEST_OPTIONS.get(flow, {}).get(node, [])) + in_text = random.choice(answers) if answers else "go to fallback" + in_message = Message(text=in_text) + await asyncio.sleep(random.random() * 3) + ctx = await pipeline._run_pipeline(in_message, ctx.id) + await asyncio.sleep(random.random() * 3) + await queue.put(ctx) + + +# %% +# main loop +async def main(n_iterations: int = 100, n_workers: int = 4): + """ + The main loop that runs one or more worker coroutines in parallel. + + :param n_iterations: Total number of coroutine runs. + :param n_workers: Number of parallelized coroutine runs. + """ + ctxs = asyncio.Queue() + parallel_iterations = n_iterations // n_workers + for _ in range(n_workers): + await ctxs.put(Context()) + for _ in tqdm(range(parallel_iterations)): + await asyncio.gather(*(worker(ctxs) for _ in range(n_workers))) + + +if __name__ == "__main__": + asyncio.run(main()) From d5335bfce45158b311fc22dbf31581771daf4f48 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 31 Oct 2023 17:50:01 +0300 Subject: [PATCH 18/22] add link to context storage tutorials in context guide --- docs/source/user_guides/context_guide.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/user_guides/context_guide.rst b/docs/source/user_guides/context_guide.rst index 464d963a7..0583aecc1 100644 --- a/docs/source/user_guides/context_guide.rst +++ b/docs/source/user_guides/context_guide.rst @@ -208,6 +208,9 @@ The function's only parameter is a connection string that specifies both the dat and the connection parameters, for example, *mongodb://admin:pass@localhost:27016/admin*. (`see the reference <../apiref/dff.context_storages.database.html#dff.context_storages.database.context_storage_factory>`_) +.. note:: + To learn how to use ``context_storage_factory`` in your pipeline, see our `Context Storage Tutorials <../tutorials/index_context_storages.html>`__. + The GitHub-based distribution of DFF includes Docker images for each of the supported database types. Therefore, the easiest way to deploy your service together with a database is to clone the GitHub distribution and to take advantage of the packaged From 398ff0e6f59dc9cbbb5335517fefa1f63a6b49f4 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Wed, 1 Nov 2023 18:25:17 +0300 Subject: [PATCH 19/22] fix query_context after time_range update --- .../charts/Flow_visit_ratio_monitor_13.yaml | 4 ++-- .../superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml b/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml index dfa036ee4..8605113de 100644 --- a/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml +++ b/dff/config/superset_dashboard/charts/Flow_visit_ratio_monitor_13.yaml @@ -90,8 +90,8 @@ params: extra_form_data: {} dashboards: - 1 -query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"DATEADD(DATETIME(\"now\"), - -1, day) : now","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label <> '''')"},"applied_time_extras":{},"columns":["flow_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":13,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"DATEADD(DATETIME(\"now\"), -1, day) : now","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_waxxrrnkwwm_zudaex5z8bh","sqlExpression":null}],"groupby":["flow_label"],"contributionMode":"column","adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_xqjhabd7z8p_05dlj2lmg6ue"},{"expressionType":"SQL","sqlExpression":"label <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_822k33tkrei_irkb2zdjds"}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title_margin":15,"y_axis_title":"","y_axis_title_margin":50,"y_axis_title_position":"Left","color_scheme":"echarts4Colors","stack":true,"only_total":true,"zoomable":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":"SMART_NUMBER","y_axis_bounds":[null,null],"rich_tooltip":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[1],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}' diff --git a/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml b/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml index 7f5852930..527b8a984 100644 --- a/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml +++ b/dff/config/superset_dashboard/charts/Node_visit_ratio_monitor_8.yaml @@ -97,8 +97,8 @@ params: extra_form_data: {} dashboards: - 1 -query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"DATEADD(DATETIME(\"now\"), - -1, day) : now","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label +query_context: '{"datasource":{"id":2,"type":"table"},"force":false,"queries":[{"time_range":"No + filter","granularity":"start_time","filters":[{"col":"data_key","op":"==","val":"get_current_label"}],"extras":{"time_grain_sqla":"PT1M","having":"","where":"(label <> '''')"},"applied_time_extras":{},"columns":["flow_label","node_label"],"metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"orderby":[[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["flow_label","node_label"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"is_timeseries":true,"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["__timestamp"],"columns":["flow_label","node_label"],"aggregates":{"COUNT(context_id)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"COUNT(context_id)":null},"level":0,"inplace":true}},{"operation":"contribution","options":{"orientation":"column"}},{"operation":"flatten"}]}],"form_data":{"datasource":"2__table","viz_type":"echarts_timeseries_bar","slice_id":2,"granularity_sqla":"start_time","time_grain_sqla":"PT1M","time_range":"DATEADD(DATETIME(\"now\"), -1, day) : now","metrics":[{"aggregate":"COUNT","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"context_id","description":null,"expression":null,"filterable":true,"groupby":true,"id":1,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"STRING","type_generic":1,"verbose_name":null,"warning_markdown":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"COUNT(context_id)","optionName":"metric_9yefk3wj2g_5wbp61n0pyr","sqlExpression":null}],"groupby":["flow_label","node_label"],"contributionMode":"column","adhoc_filters":[{"expressionType":"SIMPLE","subject":"data_key","operator":"==","operatorId":"EQUALS","comparator":"get_current_label","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_y0hyd1ebac9_flmblpn0ymt"},{"expressionType":"SQL","sqlExpression":"label <> ''''","clause":"WHERE","subject":null,"operator":null,"comparator":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_tjpviv3vvq_fdqstvdz39m"}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title":"Datetime","x_axis_title_margin":30,"y_axis_title":"Node From 832e430c8d80fbae75900459dac0210a2abb9d60 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Tue, 7 Nov 2023 02:17:05 +0300 Subject: [PATCH 20/22] increase wait time for stats test_tutorials Workflows sometimes fail at the next assert. This is most likely the reason. --- tests/stats/test_tutorials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stats/test_tutorials.py b/tests/stats/test_tutorials.py index 969dc6bf5..7a29bcbff 100644 --- a/tests/stats/test_tutorials.py +++ b/tests/stats/test_tutorials.py @@ -54,7 +54,7 @@ async def test_examples_ch(example_module_name: str, expected_logs, otlp_log_exp module.dff_instrumentor.uninstrument() module.dff_instrumentor.instrument(logger_provider=logger_provider, tracer_provider=tracer_provider) check_happy_path(pipeline, HAPPY_PATH) - await asyncio.sleep(1) + await asyncio.sleep(3) count = await ch_client.fetchval(f"SELECT COUNT (*) FROM {table}") assert count == expected_logs From a246c560af45b4da30eca7d547eafe520c61302c Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Thu, 9 Nov 2023 00:10:08 +0300 Subject: [PATCH 21/22] Add docker volumes (#270) * add docker volumes for context storages * remove stats volume names --- docker-compose.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3c88bc854..bb5cb93dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: restart: unless-stopped ports: - 3307:3306 + volumes: + - mysql-data:/var/lib/mysql psql: env_file: [.env_file] image: postgres:latest @@ -16,6 +18,8 @@ services: restart: unless-stopped ports: - 5432:5432 + volumes: + - postgres-data:/var/lib/postgresql/data redis: env_file: [.env_file] image: redis:latest @@ -25,6 +29,8 @@ services: command: --requirepass pass ports: - 6379:6379 + volumes: + - redis-data:/data mongo: env_file: [.env_file] image: mongo:latest @@ -33,6 +39,8 @@ services: restart: unless-stopped ports: - 27017:27017 + volumes: + - mongo-data:/data/db ydb: env_file: [.env_file] image: cr.yandex/yc/yandex-docker-local-ydb:latest @@ -44,6 +52,9 @@ services: # - 2135:2135 - 2136:2136 hostname: localhost + volumes: + - ydb-data:/ydb_data + - ydb-certs:/ydb_certs dashboard: env_file: [.env_file] build: @@ -112,6 +123,10 @@ services: - "4318:4318" # OTLP over HTTP receiver volumes: ch-data: - name: "ch-data" dashboard-data: - name: "dashboard-data" + mysql-data: + postgres-data: + redis-data: + mongo-data: + ydb-data: + ydb-certs: From 62dc6e83958ca2c1d7628e78ad8ae921668ad3e4 Mon Sep 17 00:00:00 2001 From: Roman Zlobin Date: Thu, 9 Nov 2023 16:58:12 +0300 Subject: [PATCH 22/22] Fix stat tests (#272) * fix test_get_current_label create a new pipeline && compare dicts * reinstrument using instrumentor from an imported module * remove unnecessary imports * reformat * replace `example` with `tutorial` in stat tests * remove unused expected from `test_otlp_integration` --- tests/stats/test_defaults.py | 44 +++++++++++++++++------------------ tests/stats/test_tutorials.py | 18 +++++++------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/stats/test_defaults.py b/tests/stats/test_defaults.py index d565efcdd..bd4982eaf 100644 --- a/tests/stats/test_defaults.py +++ b/tests/stats/test_defaults.py @@ -1,27 +1,29 @@ +import importlib + import pytest +from dff.script import Context +from dff.pipeline import Pipeline +from dff.pipeline.types import ExtraHandlerRuntimeInfo, ServiceRuntimeInfo + try: - from wrapt import wrap_function_wrapper # noqa: F401 - from dff.stats import OtelInstrumentor + from dff.stats import default_extractors except ImportError: pytest.skip(allow_module_level=True, reason="One of the Opentelemetry packages is missing.") -import importlib -from dff.script import Context -from dff.pipeline.types import ExtraHandlerRuntimeInfo, ServiceRuntimeInfo -from dff.stats import default_extractors - @pytest.mark.asyncio @pytest.mark.parametrize( "context,expected", [ - (Context(), set()), - (Context(labels={0: ("a", "b")}), {("flow", "a"), ("node", "b"), ("label", "a: b")}), + (Context(), {"flow": "greeting_flow", "label": "greeting_flow: start_node", "node": "start_node"}), + (Context(labels={0: ("a", "b")}), {"flow": "a", "node": "b", "label": "a: b"}), ], ) async def test_get_current_label(context: Context, expected: set): - example_module = importlib.import_module("tutorials.stats.1_extractor_functions") + pipeline = Pipeline.from_script( + {"greeting_flow": {"start_node": {}}}, ("greeting_flow", "start_node"), validation_stage=False + ) runtime_info = ExtraHandlerRuntimeInfo( func=lambda x: x, stage="BEFORE", @@ -29,26 +31,24 @@ async def test_get_current_label(context: Context, expected: set): path=".", name=".", timeout=None, asynchronous=False, execution_state={".": "FINISHED"} ), ) - result = await default_extractors.get_current_label(context, example_module.pipeline, runtime_info) - assert expected.intersection(set(result.items())) == expected + result = await default_extractors.get_current_label(context, pipeline, runtime_info) + assert result == expected @pytest.mark.asyncio @pytest.mark.parametrize( - "context,expected", + "context", [ - (Context(), set()), - (Context(labels={0: ("a", "b")}), {("flow", "a"), ("node", "b"), ("label", "a: b")}), + Context(), + Context(labels={0: ("a", "b")}), ], ) -async def test_otlp_integration(context, expected, tracer_exporter_and_provider, log_exporter_and_provider): +async def test_otlp_integration(context, tracer_exporter_and_provider, log_exporter_and_provider): _, tracer_provider = tracer_exporter_and_provider log_exporter, logger_provider = log_exporter_and_provider - example_module = importlib.import_module("tutorials.stats.1_extractor_functions") - instrumentor = OtelInstrumentor() - if instrumentor.is_instrumented_by_opentelemetry: - instrumentor.uninstrument() - instrumentor.instrument(logger_provider=logger_provider, tracer_provider=tracer_provider) + tutorial_module = importlib.import_module("tutorials.stats.1_extractor_functions") + tutorial_module.dff_instrumentor.uninstrument() + tutorial_module.dff_instrumentor.instrument(logger_provider=logger_provider, tracer_provider=tracer_provider) runtime_info = ExtraHandlerRuntimeInfo( func=lambda x: x, stage="BEFORE", @@ -56,7 +56,7 @@ async def test_otlp_integration(context, expected, tracer_exporter_and_provider, path=".", name=".", timeout=None, asynchronous=False, execution_state={".": "FINISHED"} ), ) - _ = await default_extractors.get_current_label(context, example_module.pipeline, runtime_info) + _ = await default_extractors.get_current_label(context, tutorial_module.pipeline, runtime_info) tracer_provider.force_flush() logger_provider.force_flush() assert len(log_exporter.get_finished_logs()) > 0 diff --git a/tests/stats/test_tutorials.py b/tests/stats/test_tutorials.py index 7a29bcbff..efa1b1748 100644 --- a/tests/stats/test_tutorials.py +++ b/tests/stats/test_tutorials.py @@ -33,15 +33,15 @@ ) @pytest.mark.asyncio @pytest.mark.parametrize( - ["example_module_name", "expected_logs"], + ["tutorial_module_name", "expected_logs"], [ ("1_extractor_functions", 10), ("2_pipeline_integration", 35), ], ) @pytest.mark.docker -async def test_examples_ch(example_module_name: str, expected_logs, otlp_log_exp_provider, otlp_trace_exp_provider): - module = importlib.import_module(f"tutorials.{dot_path_to_addon}.{example_module_name}") +async def test_tutorials_ch(tutorial_module_name: str, expected_logs, otlp_log_exp_provider, otlp_trace_exp_provider): + module = importlib.import_module(f"tutorials.{dot_path_to_addon}.{tutorial_module_name}") _, tracer_provider = otlp_trace_exp_provider _, logger_provider = otlp_log_exp_provider http_client = AsyncClient() @@ -59,21 +59,21 @@ async def test_examples_ch(example_module_name: str, expected_logs, otlp_log_exp assert count == expected_logs except Exception as exc: - raise Exception(f"model_name=tutorials.{dot_path_to_addon}.{example_module_name}") from exc + raise Exception(f"model_name=tutorials.{dot_path_to_addon}.{tutorial_module_name}") from exc @pytest.mark.asyncio @pytest.mark.parametrize( - ["example_module_name", "expected_logs"], + ["tutorial_module_name", "expected_logs"], [ ("1_extractor_functions", 10), ("2_pipeline_integration", 35), ], ) -async def test_examples_memory( - example_module_name: str, expected_logs, tracer_exporter_and_provider, log_exporter_and_provider +async def test_tutorials_memory( + tutorial_module_name: str, expected_logs, tracer_exporter_and_provider, log_exporter_and_provider ): - module = importlib.import_module(f"tutorials.{dot_path_to_addon}.{example_module_name}") + module = importlib.import_module(f"tutorials.{dot_path_to_addon}.{tutorial_module_name}") _, tracer_provider = tracer_exporter_and_provider log_exporter, logger_provider = log_exporter_and_provider try: @@ -87,4 +87,4 @@ async def test_examples_memory( assert len(log_exporter.get_finished_logs()) == expected_logs except Exception as exc: - raise Exception(f"model_name=tutorials.{dot_path_to_addon}.{example_module_name}") from exc + raise Exception(f"model_name=tutorials.{dot_path_to_addon}.{tutorial_module_name}") from exc