diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000000..8d6151f21f --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 07023cba63ba37048badfd1a7682fb0c +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/about_us.doctree b/.doctrees/about_us.doctree new file mode 100644 index 0000000000..509e065af4 Binary files /dev/null and b/.doctrees/about_us.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.database.doctree b/.doctrees/apiref/dff.context_storages.database.doctree new file mode 100644 index 0000000000..91714119e4 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.database.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.json.doctree b/.doctrees/apiref/dff.context_storages.json.doctree new file mode 100644 index 0000000000..6a9cd66931 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.json.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.mongo.doctree b/.doctrees/apiref/dff.context_storages.mongo.doctree new file mode 100644 index 0000000000..e9180b0218 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.mongo.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.pickle.doctree b/.doctrees/apiref/dff.context_storages.pickle.doctree new file mode 100644 index 0000000000..74147299f0 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.pickle.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.protocol.doctree b/.doctrees/apiref/dff.context_storages.protocol.doctree new file mode 100644 index 0000000000..7640d8c966 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.protocol.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.redis.doctree b/.doctrees/apiref/dff.context_storages.redis.doctree new file mode 100644 index 0000000000..14f5aae4cf Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.redis.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.shelve.doctree b/.doctrees/apiref/dff.context_storages.shelve.doctree new file mode 100644 index 0000000000..c340becbae Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.shelve.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.sql.doctree b/.doctrees/apiref/dff.context_storages.sql.doctree new file mode 100644 index 0000000000..fe8412f4d8 Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.sql.doctree differ diff --git a/.doctrees/apiref/dff.context_storages.ydb.doctree b/.doctrees/apiref/dff.context_storages.ydb.doctree new file mode 100644 index 0000000000..7027d7898c Binary files /dev/null and b/.doctrees/apiref/dff.context_storages.ydb.doctree differ diff --git a/.doctrees/apiref/dff.messengers.common.interface.doctree b/.doctrees/apiref/dff.messengers.common.interface.doctree new file mode 100644 index 0000000000..8855262d2e Binary files /dev/null and b/.doctrees/apiref/dff.messengers.common.interface.doctree differ diff --git a/.doctrees/apiref/dff.messengers.common.types.doctree b/.doctrees/apiref/dff.messengers.common.types.doctree new file mode 100644 index 0000000000..f6946fe0b8 Binary files /dev/null and b/.doctrees/apiref/dff.messengers.common.types.doctree differ diff --git a/.doctrees/apiref/dff.messengers.telegram.interface.doctree b/.doctrees/apiref/dff.messengers.telegram.interface.doctree new file mode 100644 index 0000000000..c84221dd16 Binary files /dev/null and b/.doctrees/apiref/dff.messengers.telegram.interface.doctree differ diff --git a/.doctrees/apiref/dff.messengers.telegram.message.doctree b/.doctrees/apiref/dff.messengers.telegram.message.doctree new file mode 100644 index 0000000000..3bc3073cb8 Binary files /dev/null and b/.doctrees/apiref/dff.messengers.telegram.message.doctree differ diff --git a/.doctrees/apiref/dff.messengers.telegram.messenger.doctree b/.doctrees/apiref/dff.messengers.telegram.messenger.doctree new file mode 100644 index 0000000000..25e0f1bf46 Binary files /dev/null and b/.doctrees/apiref/dff.messengers.telegram.messenger.doctree differ diff --git a/.doctrees/apiref/dff.messengers.telegram.utils.doctree b/.doctrees/apiref/dff.messengers.telegram.utils.doctree new file mode 100644 index 0000000000..bd3c9368ea Binary files /dev/null and b/.doctrees/apiref/dff.messengers.telegram.utils.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.conditions.doctree b/.doctrees/apiref/dff.pipeline.conditions.doctree new file mode 100644 index 0000000000..30c4d05566 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.conditions.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.pipeline.actor.doctree b/.doctrees/apiref/dff.pipeline.pipeline.actor.doctree new file mode 100644 index 0000000000..4fada6eadd Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.pipeline.actor.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.pipeline.component.doctree b/.doctrees/apiref/dff.pipeline.pipeline.component.doctree new file mode 100644 index 0000000000..5d5862a542 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.pipeline.component.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.pipeline.pipeline.doctree b/.doctrees/apiref/dff.pipeline.pipeline.pipeline.doctree new file mode 100644 index 0000000000..50bfdb76ae Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.pipeline.pipeline.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.pipeline.utils.doctree b/.doctrees/apiref/dff.pipeline.pipeline.utils.doctree new file mode 100644 index 0000000000..2a72a7942e Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.pipeline.utils.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.service.extra.doctree b/.doctrees/apiref/dff.pipeline.service.extra.doctree new file mode 100644 index 0000000000..0fc5dc41c9 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.service.extra.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.service.group.doctree b/.doctrees/apiref/dff.pipeline.service.group.doctree new file mode 100644 index 0000000000..ec41fbcf42 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.service.group.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.service.service.doctree b/.doctrees/apiref/dff.pipeline.service.service.doctree new file mode 100644 index 0000000000..6df3f78e90 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.service.service.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.service.utils.doctree b/.doctrees/apiref/dff.pipeline.service.utils.doctree new file mode 100644 index 0000000000..04d274e94d Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.service.utils.doctree differ diff --git a/.doctrees/apiref/dff.pipeline.types.doctree b/.doctrees/apiref/dff.pipeline.types.doctree new file mode 100644 index 0000000000..4fc365cca6 Binary files /dev/null and b/.doctrees/apiref/dff.pipeline.types.doctree differ diff --git a/.doctrees/apiref/dff.script.conditions.std_conditions.doctree b/.doctrees/apiref/dff.script.conditions.std_conditions.doctree new file mode 100644 index 0000000000..840d7c958f Binary files /dev/null and b/.doctrees/apiref/dff.script.conditions.std_conditions.doctree differ diff --git a/.doctrees/apiref/dff.script.core.context.doctree b/.doctrees/apiref/dff.script.core.context.doctree new file mode 100644 index 0000000000..f0a017f656 Binary files /dev/null and b/.doctrees/apiref/dff.script.core.context.doctree differ diff --git a/.doctrees/apiref/dff.script.core.keywords.doctree b/.doctrees/apiref/dff.script.core.keywords.doctree new file mode 100644 index 0000000000..fd9ba24b0b Binary files /dev/null and b/.doctrees/apiref/dff.script.core.keywords.doctree differ diff --git a/.doctrees/apiref/dff.script.core.message.doctree b/.doctrees/apiref/dff.script.core.message.doctree new file mode 100644 index 0000000000..3b98886fdc Binary files /dev/null and b/.doctrees/apiref/dff.script.core.message.doctree differ diff --git a/.doctrees/apiref/dff.script.core.normalization.doctree b/.doctrees/apiref/dff.script.core.normalization.doctree new file mode 100644 index 0000000000..35211849a8 Binary files /dev/null and b/.doctrees/apiref/dff.script.core.normalization.doctree differ diff --git a/.doctrees/apiref/dff.script.core.script.doctree b/.doctrees/apiref/dff.script.core.script.doctree new file mode 100644 index 0000000000..e226a3dc70 Binary files /dev/null and b/.doctrees/apiref/dff.script.core.script.doctree differ diff --git a/.doctrees/apiref/dff.script.core.types.doctree b/.doctrees/apiref/dff.script.core.types.doctree new file mode 100644 index 0000000000..e46b17baf9 Binary files /dev/null and b/.doctrees/apiref/dff.script.core.types.doctree differ diff --git a/.doctrees/apiref/dff.script.extras.conditions.doctree b/.doctrees/apiref/dff.script.extras.conditions.doctree new file mode 100644 index 0000000000..c7990f2942 Binary files /dev/null and b/.doctrees/apiref/dff.script.extras.conditions.doctree differ diff --git a/.doctrees/apiref/dff.script.extras.slots.doctree b/.doctrees/apiref/dff.script.extras.slots.doctree new file mode 100644 index 0000000000..1b6c101574 Binary files /dev/null and b/.doctrees/apiref/dff.script.extras.slots.doctree differ diff --git a/.doctrees/apiref/dff.script.labels.std_labels.doctree b/.doctrees/apiref/dff.script.labels.std_labels.doctree new file mode 100644 index 0000000000..d921728734 Binary files /dev/null and b/.doctrees/apiref/dff.script.labels.std_labels.doctree differ diff --git a/.doctrees/apiref/dff.script.responses.std_responses.doctree b/.doctrees/apiref/dff.script.responses.std_responses.doctree new file mode 100644 index 0000000000..b89060e6da Binary files /dev/null and b/.doctrees/apiref/dff.script.responses.std_responses.doctree differ diff --git a/.doctrees/apiref/dff.stats.cli.doctree b/.doctrees/apiref/dff.stats.cli.doctree new file mode 100644 index 0000000000..57a4ef330e Binary files /dev/null and b/.doctrees/apiref/dff.stats.cli.doctree differ diff --git a/.doctrees/apiref/dff.stats.default_extractors.doctree b/.doctrees/apiref/dff.stats.default_extractors.doctree new file mode 100644 index 0000000000..7fabb21643 Binary files /dev/null and b/.doctrees/apiref/dff.stats.default_extractors.doctree differ diff --git a/.doctrees/apiref/dff.stats.instrumentor.doctree b/.doctrees/apiref/dff.stats.instrumentor.doctree new file mode 100644 index 0000000000..a3180f8e11 Binary files /dev/null and b/.doctrees/apiref/dff.stats.instrumentor.doctree differ diff --git a/.doctrees/apiref/dff.stats.utils.doctree b/.doctrees/apiref/dff.stats.utils.doctree new file mode 100644 index 0000000000..47b4471df9 Binary files /dev/null and b/.doctrees/apiref/dff.stats.utils.doctree differ diff --git a/.doctrees/apiref/dff.utils.db_benchmark.basic_config.doctree b/.doctrees/apiref/dff.utils.db_benchmark.basic_config.doctree new file mode 100644 index 0000000000..cd097c602f Binary files /dev/null and b/.doctrees/apiref/dff.utils.db_benchmark.basic_config.doctree differ diff --git a/.doctrees/apiref/dff.utils.db_benchmark.benchmark.doctree b/.doctrees/apiref/dff.utils.db_benchmark.benchmark.doctree new file mode 100644 index 0000000000..6f6db401f6 Binary files /dev/null and b/.doctrees/apiref/dff.utils.db_benchmark.benchmark.doctree differ diff --git a/.doctrees/apiref/dff.utils.db_benchmark.report.doctree b/.doctrees/apiref/dff.utils.db_benchmark.report.doctree new file mode 100644 index 0000000000..c45d54b232 Binary files /dev/null and b/.doctrees/apiref/dff.utils.db_benchmark.report.doctree differ diff --git a/.doctrees/apiref/dff.utils.testing.cleanup_db.doctree b/.doctrees/apiref/dff.utils.testing.cleanup_db.doctree new file mode 100644 index 0000000000..2a9a6b9019 Binary files /dev/null and b/.doctrees/apiref/dff.utils.testing.cleanup_db.doctree differ diff --git a/.doctrees/apiref/dff.utils.testing.common.doctree b/.doctrees/apiref/dff.utils.testing.common.doctree new file mode 100644 index 0000000000..057836c440 Binary files /dev/null and b/.doctrees/apiref/dff.utils.testing.common.doctree differ diff --git a/.doctrees/apiref/dff.utils.testing.response_comparers.doctree b/.doctrees/apiref/dff.utils.testing.response_comparers.doctree new file mode 100644 index 0000000000..975b470561 Binary files /dev/null and b/.doctrees/apiref/dff.utils.testing.response_comparers.doctree differ diff --git a/.doctrees/apiref/dff.utils.testing.telegram.doctree b/.doctrees/apiref/dff.utils.testing.telegram.doctree new file mode 100644 index 0000000000..769398d312 Binary files /dev/null and b/.doctrees/apiref/dff.utils.testing.telegram.doctree differ diff --git a/.doctrees/apiref/dff.utils.testing.toy_script.doctree b/.doctrees/apiref/dff.utils.testing.toy_script.doctree new file mode 100644 index 0000000000..c47fbdb3ff Binary files /dev/null and b/.doctrees/apiref/dff.utils.testing.toy_script.doctree differ diff --git a/.doctrees/apiref/dff.utils.turn_caching.singleton_turn_caching.doctree b/.doctrees/apiref/dff.utils.turn_caching.singleton_turn_caching.doctree new file mode 100644 index 0000000000..48116b0125 Binary files /dev/null and b/.doctrees/apiref/dff.utils.turn_caching.singleton_turn_caching.doctree differ diff --git a/.doctrees/apiref/index_caching.doctree b/.doctrees/apiref/index_caching.doctree new file mode 100644 index 0000000000..010290586f Binary files /dev/null and b/.doctrees/apiref/index_caching.doctree differ diff --git a/.doctrees/apiref/index_context_storages.doctree b/.doctrees/apiref/index_context_storages.doctree new file mode 100644 index 0000000000..2b1bc64139 Binary files /dev/null and b/.doctrees/apiref/index_context_storages.doctree differ diff --git a/.doctrees/apiref/index_db_benchmark.doctree b/.doctrees/apiref/index_db_benchmark.doctree new file mode 100644 index 0000000000..063d1d89a0 Binary files /dev/null and b/.doctrees/apiref/index_db_benchmark.doctree differ diff --git a/.doctrees/apiref/index_messenger_interfaces.doctree b/.doctrees/apiref/index_messenger_interfaces.doctree new file mode 100644 index 0000000000..b71d1d45bd Binary files /dev/null and b/.doctrees/apiref/index_messenger_interfaces.doctree differ diff --git a/.doctrees/apiref/index_pipeline.doctree b/.doctrees/apiref/index_pipeline.doctree new file mode 100644 index 0000000000..bcce87871b Binary files /dev/null and b/.doctrees/apiref/index_pipeline.doctree differ diff --git a/.doctrees/apiref/index_script.doctree b/.doctrees/apiref/index_script.doctree new file mode 100644 index 0000000000..17d1b3fe73 Binary files /dev/null and b/.doctrees/apiref/index_script.doctree differ diff --git a/.doctrees/apiref/index_stats.doctree b/.doctrees/apiref/index_stats.doctree new file mode 100644 index 0000000000..b4ab8e629e Binary files /dev/null and b/.doctrees/apiref/index_stats.doctree differ diff --git a/.doctrees/apiref/index_testing_utils.doctree b/.doctrees/apiref/index_testing_utils.doctree new file mode 100644 index 0000000000..f68de863a9 Binary files /dev/null and b/.doctrees/apiref/index_testing_utils.doctree differ diff --git a/.doctrees/community.doctree b/.doctrees/community.doctree new file mode 100644 index 0000000000..3beb5f59f3 Binary files /dev/null and b/.doctrees/community.doctree differ diff --git a/.doctrees/development.doctree b/.doctrees/development.doctree new file mode 100644 index 0000000000..e9a385c639 Binary files /dev/null and b/.doctrees/development.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 0000000000..4dd5dc00ad Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/examples.doctree b/.doctrees/examples.doctree new file mode 100644 index 0000000000..2b220a1861 Binary files /dev/null and b/.doctrees/examples.doctree differ diff --git a/.doctrees/get_started.doctree b/.doctrees/get_started.doctree new file mode 100644 index 0000000000..bffa03fc9b Binary files /dev/null and b/.doctrees/get_started.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 0000000000..3432f2f117 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.1_basics.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.1_basics.ipynb new file mode 100644 index 0000000000..432c1d2da6 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.1_basics.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cf528607", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 1. Basics\n", + "\n", + "The following tutorial shows the basic use of the database connection.\n", + "\n", + "See [context_storage_factory](../apiref/dff.context_storages.database.rst#dff.context_storages.database.context_storage_factory) function\n", + "for creating a context storage by path.\n", + "\n", + "In this example JSON file is used as a storage." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "785bff8a", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:16.014905Z", + "iopub.status.busy": "2023-12-27T16:49:16.014247Z", + "iopub.status.idle": "2023-12-27T16:49:18.400779Z", + "shell.execute_reply": "2023-12-27T16:49:18.399622Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[json,pickle]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eca435ac", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:18.405873Z", + "iopub.status.busy": "2023-12-27T16:49:18.405562Z", + "iopub.status.idle": "2023-12-27T16:49:19.318046Z", + "shell.execute_reply": "2023-12-27T16:49:19.317352Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "import pathlib\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH\n", + "\n", + "pathlib.Path(\"dbs\").mkdir(exist_ok=True)\n", + "db = context_storage_factory(\"json://dbs/file.json\")\n", + "# db = context_storage_factory(\"pickle://dbs/file.pkl\")\n", + "# db = context_storage_factory(\"shelve://dbs/file\")\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " # a function for automatic tutorial running (testing) with HAPPY_PATH\n", + "\n", + " # This runs tutorial in interactive mode if not in IPython env\n", + " # and if `DISABLE_INTERACTIVE_MODE` is not set\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline) # This runs tutorial in interactive mode" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.2_postgresql.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.2_postgresql.ipynb new file mode 100644 index 0000000000..91ee6f30c8 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.2_postgresql.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b2141428", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 2. PostgreSQL\n", + "\n", + "This is a tutorial on using PostgreSQL.\n", + "\n", + "See [SQLContextStorage](../apiref/dff.context_storages.sql.rst#dff.context_storages.sql.SQLContextStorage) class\n", + "for storing your users' contexts in SQL databases.\n", + "\n", + "DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/)\n", + "and [asyncpg](https://magicstack.github.io/asyncpg/current/)\n", + "libraries for asynchronous access to PostgreSQL DB." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6ae2042c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:20.999153Z", + "iopub.status.busy": "2023-12-27T16:49:20.998957Z", + "iopub.status.idle": "2023-12-27T16:49:23.325035Z", + "shell.execute_reply": "2023-12-27T16:49:23.324227Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[postgresql]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5eb6b286", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:23.328061Z", + "iopub.status.busy": "2023-12-27T16:49:23.327778Z", + "iopub.status.idle": "2023-12-27T16:49:24.063795Z", + "shell.execute_reply": "2023-12-27T16:49:24.062861Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95189175", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:24.068078Z", + "iopub.status.busy": "2023-12-27T16:49:24.067471Z", + "iopub.status.idle": "2023-12-27T16:49:24.186954Z", + "shell.execute_reply": "2023-12-27T16:49:24.186204Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "db_uri = \"postgresql+asyncpg://{}:{}@localhost:5432/{}\".format(\n", + " os.environ[\"POSTGRES_USERNAME\"],\n", + " os.environ[\"POSTGRES_PASSWORD\"],\n", + " os.environ[\"POSTGRES_DB\"],\n", + ")\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5dd744bb", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:24.189978Z", + "iopub.status.busy": "2023-12-27T16:49:24.189769Z", + "iopub.status.idle": "2023-12-27T16:49:24.216608Z", + "shell.execute_reply": "2023-12-27T16:49:24.215746Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.3_mongodb.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.3_mongodb.ipynb new file mode 100644 index 0000000000..b6604fc111 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.3_mongodb.ipynb @@ -0,0 +1,163 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "272e672c", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 3. MongoDB\n", + "\n", + "This is a tutorial on using MongoDB.\n", + "\n", + "See [MongoContextStorage](../apiref/dff.context_storages.mongo.rst#dff.context_storages.mongo.MongoContextStorage) class\n", + "for storing you users' contexts in Mongo database.\n", + "\n", + "DFF uses [motor](https://motor.readthedocs.io/en/stable/)\n", + "library for asynchronous access to MongoDB." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "be2ea876", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:25.915710Z", + "iopub.status.busy": "2023-12-27T16:49:25.915045Z", + "iopub.status.idle": "2023-12-27T16:49:28.203059Z", + "shell.execute_reply": "2023-12-27T16:49:28.202308Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[mongodb]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f83f1402", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:28.206123Z", + "iopub.status.busy": "2023-12-27T16:49:28.205686Z", + "iopub.status.idle": "2023-12-27T16:49:28.907742Z", + "shell.execute_reply": "2023-12-27T16:49:28.907075Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ee8fa71b", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:28.911043Z", + "iopub.status.busy": "2023-12-27T16:49:28.910554Z", + "iopub.status.idle": "2023-12-27T16:49:29.002287Z", + "shell.execute_reply": "2023-12-27T16:49:29.001542Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "db_uri = \"mongodb://{}:{}@localhost:27017/{}\".format(\n", + " os.environ[\"MONGO_INITDB_ROOT_USERNAME\"],\n", + " os.environ[\"MONGO_INITDB_ROOT_PASSWORD\"],\n", + " os.environ[\"MONGO_INITDB_ROOT_USERNAME\"],\n", + ")\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "365916c9", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:29.005493Z", + "iopub.status.busy": "2023-12-27T16:49:29.005055Z", + "iopub.status.idle": "2023-12-27T16:49:29.036501Z", + "shell.execute_reply": "2023-12-27T16:49:29.035811Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.4_redis.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.4_redis.ipynb new file mode 100644 index 0000000000..2c01e257d0 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.4_redis.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4f99c29d", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 4. Redis\n", + "\n", + "This is a tutorial on using Redis.\n", + "\n", + "See [RedisContextStorage](../apiref/dff.context_storages.redis.rst#dff.context_storages.redis.RedisContextStorage) class\n", + "for storing you users' contexts in Redis database.\n", + "\n", + "DFF uses [redis.asyncio](https://redis.readthedocs.io/en/latest/)\n", + "library for asynchronous access to Redis DB." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b0de4e3f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:31.454397Z", + "iopub.status.busy": "2023-12-27T16:49:31.453837Z", + "iopub.status.idle": "2023-12-27T16:49:33.773016Z", + "shell.execute_reply": "2023-12-27T16:49:33.772252Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[redis]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "24b423b4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:33.776076Z", + "iopub.status.busy": "2023-12-27T16:49:33.775641Z", + "iopub.status.idle": "2023-12-27T16:49:34.476653Z", + "shell.execute_reply": "2023-12-27T16:49:34.475930Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3f9ffd3a", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:34.479878Z", + "iopub.status.busy": "2023-12-27T16:49:34.479372Z", + "iopub.status.idle": "2023-12-27T16:49:34.484198Z", + "shell.execute_reply": "2023-12-27T16:49:34.483470Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "db_uri = \"redis://{}:{}@localhost:6379/{}\".format(\n", + " \"\", os.environ[\"REDIS_PASSWORD\"], \"0\"\n", + ")\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4e27a64c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:34.486791Z", + "iopub.status.busy": "2023-12-27T16:49:34.486400Z", + "iopub.status.idle": "2023-12-27T16:49:34.499578Z", + "shell.execute_reply": "2023-12-27T16:49:34.498959Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.5_mysql.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.5_mysql.ipynb new file mode 100644 index 0000000000..38035cd5d0 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.5_mysql.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b7b9b39f", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 5. MySQL\n", + "\n", + "This is a tutorial on using MySQL.\n", + "\n", + "See [SQLContextStorage](../apiref/dff.context_storages.sql.rst#dff.context_storages.sql.SQLContextStorage) class\n", + "for storing you users' contexts in SQL databases.\n", + "\n", + "DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/)\n", + "and [asyncmy](https://github.com/long2ice/asyncmy)\n", + "libraries for asynchronous access to MySQL DB." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bbd07a65", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:36.378420Z", + "iopub.status.busy": "2023-12-27T16:49:36.378066Z", + "iopub.status.idle": "2023-12-27T16:49:38.742011Z", + "shell.execute_reply": "2023-12-27T16:49:38.741161Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[mysql]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b46e038d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:38.745300Z", + "iopub.status.busy": "2023-12-27T16:49:38.744704Z", + "iopub.status.idle": "2023-12-27T16:49:39.430695Z", + "shell.execute_reply": "2023-12-27T16:49:39.430070Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b96328b9", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:39.433960Z", + "iopub.status.busy": "2023-12-27T16:49:39.433374Z", + "iopub.status.idle": "2023-12-27T16:49:39.476986Z", + "shell.execute_reply": "2023-12-27T16:49:39.476295Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "db_uri = \"mysql+asyncmy://{}:{}@localhost:3307/{}\".format(\n", + " os.environ[\"MYSQL_USERNAME\"],\n", + " os.environ[\"MYSQL_PASSWORD\"],\n", + " os.environ[\"MYSQL_DATABASE\"],\n", + ")\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7148d7b7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:39.480248Z", + "iopub.status.busy": "2023-12-27T16:49:39.479776Z", + "iopub.status.idle": "2023-12-27T16:49:39.510542Z", + "shell.execute_reply": "2023-12-27T16:49:39.509883Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.6_sqlite.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.6_sqlite.ipynb new file mode 100644 index 0000000000..e9e68a95c6 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.6_sqlite.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e1d4d187", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 6. SQLite\n", + "\n", + "This is a tutorial on using SQLite.\n", + "\n", + "See [SQLContextStorage](../apiref/dff.context_storages.sql.rst#dff.context_storages.sql.SQLContextStorage) class\n", + "for storing you users' contexts in SQL databases.\n", + "\n", + "DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/)\n", + "and [aiosqlite](https://readthedocs.org/projects/aiosqlite/)\n", + "libraries for asynchronous access to SQLite DB.\n", + "\n", + "Note that protocol separator for windows differs from one for linux." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f3fddd8b", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:41.268874Z", + "iopub.status.busy": "2023-12-27T16:49:41.268214Z", + "iopub.status.idle": "2023-12-27T16:49:43.772277Z", + "shell.execute_reply": "2023-12-27T16:49:43.771473Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[sqlite]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7c574825", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:43.775507Z", + "iopub.status.busy": "2023-12-27T16:49:43.774998Z", + "iopub.status.idle": "2023-12-27T16:49:44.468824Z", + "shell.execute_reply": "2023-12-27T16:49:44.468138Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import pathlib\n", + "from platform import system\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2d9dd04f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:44.472098Z", + "iopub.status.busy": "2023-12-27T16:49:44.471575Z", + "iopub.status.idle": "2023-12-27T16:49:44.492461Z", + "shell.execute_reply": "2023-12-27T16:49:44.491875Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "pathlib.Path(\"dbs\").mkdir(exist_ok=True)\n", + "db_file = pathlib.Path(\"dbs/sqlite.db\")\n", + "db_file.touch(exist_ok=True)\n", + "\n", + "separator = \"///\" if system() == \"Windows\" else \"////\"\n", + "db_uri = f\"sqlite+aiosqlite:{separator}{db_file.absolute()}\"\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "10bc4905", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:44.495241Z", + "iopub.status.busy": "2023-12-27T16:49:44.494908Z", + "iopub.status.idle": "2023-12-27T16:49:44.529817Z", + "shell.execute_reply": "2023-12-27T16:49:44.529098Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.7_yandex_database.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.7_yandex_database.ipynb new file mode 100644 index 0000000000..e47a1752c5 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.7_yandex_database.ipynb @@ -0,0 +1,170 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "40a3b7b3", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 7. Yandex DataBase\n", + "\n", + "This is a tutorial on how to use Yandex DataBase.\n", + "\n", + "See [YDBContextStorage](../apiref/dff.context_storages.ydb.rst#dff.context_storages.ydb.YDBContextStorage) class\n", + "for storing you users' contexts in Yandex database.\n", + "\n", + "DFF uses [ydb.aio](https://ydb.tech/en/docs/)\n", + "library for asynchronous access to Yandex DB." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a5f0795d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:46.307064Z", + "iopub.status.busy": "2023-12-27T16:49:46.306563Z", + "iopub.status.idle": "2023-12-27T16:49:48.760531Z", + "shell.execute_reply": "2023-12-27T16:49:48.759692Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[ydb]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "78bc601c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:48.763570Z", + "iopub.status.busy": "2023-12-27T16:49:48.763295Z", + "iopub.status.idle": "2023-12-27T16:49:49.482900Z", + "shell.execute_reply": "2023-12-27T16:49:49.482249Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from dff.context_storages import context_storage_factory\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " run_interactive_mode,\n", + " is_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "729f1c20", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:49.486161Z", + "iopub.status.busy": "2023-12-27T16:49:49.485710Z", + "iopub.status.idle": "2023-12-27T16:49:49.520593Z", + "shell.execute_reply": "2023-12-27T16:49:49.519787Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# ##### Connecting to yandex cloud\n", + "# https://github.com/zinal/ydb-python-sdk/blob/ex_basic-example_p1/examples/basic_example_v1/README.md\n", + "# export YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS=$HOME/key-ydb-sa-0.json\n", + "# export YDB_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135\n", + "# export YDB_DATABASE=/ru-central1/qwertyuiopasdfgh/123456789qwertyui\n", + "# ##### or use local-ydb with variables from .env_file\n", + "# db_uri=\"grpc://localhost:2136/local\"\n", + "\n", + "db_uri = \"{}{}\".format(\n", + " os.environ[\"YDB_ENDPOINT\"],\n", + " os.environ[\"YDB_DATABASE\"],\n", + ")\n", + "db = context_storage_factory(db_uri)\n", + "\n", + "pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "17789555", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:49.523884Z", + "iopub.status.busy": "2023-12-27T16:49:49.523643Z", + "iopub.status.idle": "2023-12-27T16:49:49.568114Z", + "shell.execute_reply": "2023-12-27T16:49:49.567375Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.context_storages.8_db_benchmarking.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.8_db_benchmarking.ipynb new file mode 100644 index 0000000000..6d9b813224 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.context_storages.8_db_benchmarking.ipynb @@ -0,0 +1,7231 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "90e41ac7", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 8. Context storage benchmarking\n", + "\n", + "This tutorial shows how to benchmark context storages.\n", + "\n", + "For more info see [API reference](../apiref/dff.utils.db_benchmark.benchmark.rst)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f5b7bace", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:51.357969Z", + "iopub.status.busy": "2023-12-27T16:49:51.357444Z", + "iopub.status.idle": "2023-12-27T16:49:54.162793Z", + "shell.execute_reply": "2023-12-27T16:49:54.161957Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[benchmark,json,pickle,postgresql,mongodb,redis,mysql,sqlite,ydb]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "22fa2e5e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:54.165881Z", + "iopub.status.busy": "2023-12-27T16:49:54.165433Z", + "iopub.status.idle": "2023-12-27T16:49:54.968118Z", + "shell.execute_reply": "2023-12-27T16:49:54.967249Z" + } + }, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from platform import system\n", + "import tempfile\n", + "\n", + "import dff.utils.db_benchmark as benchmark" + ] + }, + { + "cell_type": "markdown", + "id": "b21c945c", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "## Context storage setup" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d6efaf8c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:54.973382Z", + "iopub.status.busy": "2023-12-27T16:49:54.971976Z", + "iopub.status.idle": "2023-12-27T16:49:54.978619Z", + "shell.execute_reply": "2023-12-27T16:49:54.977940Z" + } + }, + "outputs": [], + "source": [ + "# this cell is only required for pickle, shelve and sqlite databases\n", + "tutorial_dir = Path(tempfile.mkdtemp())\n", + "db_path = tutorial_dir / \"dbs\"\n", + "db_path.mkdir()\n", + "sqlite_file = db_path / \"sqlite.db\"\n", + "sqlite_file.touch(exist_ok=True)\n", + "sqlite_separator = \"///\" if system() == \"Windows\" else \"////\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "dd1bc962", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:54.983792Z", + "iopub.status.busy": "2023-12-27T16:49:54.982432Z", + "iopub.status.idle": "2023-12-27T16:49:54.988798Z", + "shell.execute_reply": "2023-12-27T16:49:54.988088Z" + } + }, + "outputs": [], + "source": [ + "storages = {\n", + " \"JSON\": f\"json://{db_path}/json.json\",\n", + " \"Pickle\": f\"pickle://{db_path}/pickle.pkl\",\n", + " \"Shelve\": f\"shelve://{db_path}/shelve\",\n", + " \"PostgreSQL\": \"postgresql+asyncpg://postgres:pass@localhost:5432/test\",\n", + " \"MongoDB\": \"mongodb://admin:pass@localhost:27017/admin\",\n", + " \"Redis\": \"redis://:pass@localhost:6379/0\",\n", + " \"MySQL\": \"mysql+asyncmy://root:pass@localhost:3307/test\",\n", + " \"SQLite\": f\"sqlite+aiosqlite:{sqlite_separator}{sqlite_file.absolute()}\",\n", + " \"YDB\": \"grpc://localhost:2136/local\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "57cbed02", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "## Saving benchmark results to a file\n", + "\n", + "Benchmark results are saved to files.\n", + "\n", + "For that there exist two functions:\n", + "[benchmark_all](../apiref/dff.utils.db_benchmark.benchmark.rst#dff.utils.db_benchmark.benchmark.benchmark_all)\n", + "and\n", + "[save_results_to_file](../apiref/dff.utils.db_benchmark.benchmark.rst#dff.utils.db_benchmark.benchmark.save_results_to_file).\n", + "\n", + "Note: context storages passed into these functions will be cleared.\n", + "\n", + "Once the benchmark results are saved to a file, you can view and analyze them using two methods:\n", + "\n", + "* [Using the Report Function](#Using-the-report-function): This function\n", + " can display specified information from a given file.\n", + " By default, it prints the name and average metrics for each benchmark case.\n", + "\n", + "* [Using the Streamlit App](#Using-Streamlit-app): A Streamlit app\n", + " is available for viewing and comparing benchmark results.\n", + " You can upload benchmark result files using the app's \"Benchmark sets\" tab,\n", + " inspect individual results in the \"View\" tab, and compare metrics in the \"Compare\" tab.\n", + "\n", + "Benchmark results are saved according to a specific schema,\n", + "which can be found in the benchmark schema documentation.\n", + "Each database being benchmarked will have its own result file.\n", + "\n", + "### Configuration\n", + "\n", + "The first one is a higher-level wrapper of the second one.\n", + "The first function accepts\n", + "[BenchmarkCase](../apiref/dff.utils.db_benchmark.benchmark.rst#dff.utils.db_benchmark.benchmark.BenchmarkCase)\n", + "which configure databases that are being benchmark and configurations of the benchmarks.\n", + "The second function accepts only a single URI for the database and several benchmark configurations.\n", + "So, the second function is simpler to use, while the first function allows for more configuration\n", + "(e.g. having different databases benchmarked in a single file).\n", + "\n", + "Both function use\n", + "[BenchmarkConfig](../apiref/dff.utils.db_benchmark.benchmark.rst#dff.utils.db_benchmark.benchmark.BenchmarkConfig)\n", + "to configure benchmark behaviour.\n", + "`BenchmarkConfig` is only an interface for benchmark configurations.\n", + "Its most basic implementation is\n", + "[BasicBenchmarkConfig](../apiref/dff.utils.db_benchmark.basic_config.rst#dff.utils.db_benchmark.basic_config.BasicBenchmarkConfig).\n", + "\n", + "DFF provides configuration presets in the\n", + "[basic_config](../apiref/dff.utils.db_benchmark.basic_config.rst) module,\n", + "covering various contexts, messages, and edge cases.\n", + "You can use these presets by passing them to the benchmark functions or create\n", + "your own configuration.\n", + "\n", + "To learn more about using presets see [Configuration presets](#Configuration-presets)\n", + "\n", + "Benchmark configs have several parameters:\n", + "\n", + "Setting `context_num` to 50 means that we'll run fifty cycles of writing and reading context.\n", + "This way we'll be able to get a more accurate average read/write time as well as\n", + "check if read/write times are dependent on the number of contexts in the storage.\n", + "\n", + "You can also configure the `dialog_len`, `message_dimensions` and `misc_dimensions` parameters.\n", + "This allows you to set the contexts you want your database to be benchmarked with.\n", + "\n", + "### File structure\n", + "\n", + "The files are saved according to [the schema](\n", + "../_misc/benchmark_schema.json\n", + ")." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2a0c700f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:49:54.993985Z", + "iopub.status.busy": "2023-12-27T16:49:54.992630Z", + "iopub.status.idle": "2023-12-27T16:50:00.827059Z", + "shell.execute_reply": "2023-12-27T16:50:00.826273Z" + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7721373162194f93af84cd21d7b8b89f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00 ... for this example, we'll use a very simple HTML document with some JavaScript,\n", + "> all inside a long string.\n", + "> This, of course, is not optimal and you wouldn't use it for production.\n", + "\n", + "Here, [CallbackMessengerInterface](../apiref/dff.messengers.common.interface.rst#dff.messengers.common.interface.CallbackMessengerInterface)\n", + "is used to process requests.\n", + "\n", + "[Message](../apiref/dff.script.core.message.rst#dff.script.core.message.Message) is used to represent text messages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "49b0a8f3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:49.745733Z", + "iopub.status.busy": "2023-12-27T16:50:49.745158Z", + "iopub.status.idle": "2023-12-27T16:50:52.118403Z", + "shell.execute_reply": "2023-12-27T16:50:52.117596Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff uvicorn fastapi" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6ae07506", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:52.121759Z", + "iopub.status.busy": "2023-12-27T16:50:52.121228Z", + "iopub.status.idle": "2023-12-27T16:50:53.191665Z", + "shell.execute_reply": "2023-12-27T16:50:53.190932Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from dff.messengers.common.interface import CallbackMessengerInterface\n", + "from dff.script import Message\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing import TOY_SCRIPT_ARGS, is_interactive_mode\n", + "\n", + "import uvicorn\n", + "from fastapi import FastAPI, WebSocket, WebSocketDisconnect\n", + "from fastapi.responses import HTMLResponse" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "601ace23", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:53.195204Z", + "iopub.status.busy": "2023-12-27T16:50:53.194532Z", + "iopub.status.idle": "2023-12-27T16:50:53.198836Z", + "shell.execute_reply": "2023-12-27T16:50:53.198304Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "messenger_interface = CallbackMessengerInterface()\n", + "pipeline = Pipeline.from_script(\n", + " *TOY_SCRIPT_ARGS, messenger_interface=messenger_interface\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7cd877b7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:53.201438Z", + "iopub.status.busy": "2023-12-27T16:50:53.201105Z", + "iopub.status.idle": "2023-12-27T16:50:53.208177Z", + "shell.execute_reply": "2023-12-27T16:50:53.207559Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "app = FastAPI()\n", + "\n", + "html = \"\"\"\n", + "\n", + "\n", + " \n", + " Chat\n", + " \n", + " \n", + "

WebSocket Chat

\n", + "
\n", + " \n", + " \n", + "
\n", + " \n", + " \n", + " \n", + "\n", + "\"\"\"\n", + "\n", + "\n", + "@app.get(\"/\")\n", + "async def get():\n", + " return HTMLResponse(html)\n", + "\n", + "\n", + "@app.websocket(\"/ws/{client_id}\")\n", + "async def websocket_endpoint(websocket: WebSocket, client_id: int):\n", + " await websocket.accept()\n", + " try:\n", + " while True:\n", + " data = await websocket.receive_text()\n", + " await websocket.send_text(f\"User: {data}\")\n", + " request = Message(text=data)\n", + " context = await messenger_interface.on_request_async(\n", + " request, client_id\n", + " )\n", + " response = context.last_response.text\n", + " if response is not None:\n", + " await websocket.send_text(f\"Bot: {response}\")\n", + " else:\n", + " await websocket.send_text(\"Bot did not return text.\")\n", + " except WebSocketDisconnect: # ignore disconnections\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4cfbc295", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:53.210658Z", + "iopub.status.busy": "2023-12-27T16:50:53.210269Z", + "iopub.status.idle": "2023-12-27T16:50:53.213754Z", + "shell.execute_reply": "2023-12-27T16:50:53.213154Z" + } + }, + "outputs": [], + "source": [ + "if __name__ == \"__main__\":\n", + " if is_interactive_mode(): # do not run this during doc building\n", + " pipeline.run()\n", + " uvicorn.run(\n", + " app,\n", + " host=\"127.0.0.1\",\n", + " port=8000,\n", + " )" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.ipynb new file mode 100644 index 0000000000..a7560b20d2 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "31a069e6", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Web API: 3. Load testing with Locust\n", + "\n", + "This tutorial shows how to use an API endpoint created in the FastAPI tutorial in load testing." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fa9a226b", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:54.860626Z", + "iopub.status.busy": "2023-12-27T16:50:54.860405Z", + "iopub.status.idle": "2023-12-27T16:50:57.236631Z", + "shell.execute_reply": "2023-12-27T16:50:57.235703Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff locust" + ] + }, + { + "cell_type": "markdown", + "id": "73cc2762", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "## Running Locust\n", + "\n", + "1. Run this file directly:\n", + " ```bash\n", + " python {file_name}\n", + " ```\n", + "2. Run locust targeting this file:\n", + " ```bash\n", + " locust -f {file_name}\n", + " ```\n", + "3. Run from python:\n", + " ```python\n", + " import sys\n", + " from locust import main\n", + "\n", + " sys.argv = [\"locust\", \"-f\", {file_name}]\n", + " main.main()\n", + " ```\n", + "\n", + "You should see the result at http://127.0.0.1:8089.\n", + "\n", + "Make sure that your POST endpoint is also running (run the FastAPI tutorial)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "45d024d6", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:57.240333Z", + "iopub.status.busy": "2023-12-27T16:50:57.239686Z", + "iopub.status.idle": "2023-12-27T16:50:57.807146Z", + "shell.execute_reply": "2023-12-27T16:50:57.802300Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "################################################################################\n", + "# this patch is only needed to run this file in IPython kernel\n", + "# and can be safely removed\n", + "import gevent.monkey\n", + "\n", + "gevent.monkey.patch_all()\n", + "################################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "11e4e115", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:57.810314Z", + "iopub.status.busy": "2023-12-27T16:50:57.810073Z", + "iopub.status.idle": "2023-12-27T16:50:58.754052Z", + "shell.execute_reply": "2023-12-27T16:50:58.738006Z" + } + }, + "outputs": [], + "source": [ + "import uuid\n", + "import time\n", + "import sys\n", + "\n", + "from locust import FastHttpUser, task, constant, main\n", + "\n", + "from dff.script import Message\n", + "from dff.utils.testing import HAPPY_PATH, is_interactive_mode" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3dcf13bc", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:58.757661Z", + "iopub.status.busy": "2023-12-27T16:50:58.757104Z", + "iopub.status.idle": "2023-12-27T16:50:58.822538Z", + "shell.execute_reply": "2023-12-27T16:50:58.796579Z" + } + }, + "outputs": [], + "source": [ + "class DFFUser(FastHttpUser):\n", + " wait_time = constant(1)\n", + "\n", + " def check_happy_path(self, happy_path):\n", + " \"\"\"\n", + " Check a happy path.\n", + " For each `(request, response)` pair in `happy_path`:\n", + " 1. Send request to the API endpoint and catch its response.\n", + " 2. Compare API response with the `response`.\n", + " If they do not match, fail the request.\n", + "\n", + " :param happy_path:\n", + " An iterable of tuples of\n", + " `(Message, Message | Callable(Message->str|None) | None)`.\n", + "\n", + " If the second element is `Message`,\n", + " check that API response matches it.\n", + "\n", + " If the second element is `None`,\n", + " do not check the API response.\n", + "\n", + " If the second element is a `Callable`,\n", + " call it with the API response as its argument.\n", + " If the function returns a string,\n", + " that string is considered an error message.\n", + " If the function returns `None`,\n", + " the API response is considered correct.\n", + " \"\"\"\n", + " user_id = str(uuid.uuid4())\n", + "\n", + " for request, response in happy_path:\n", + " with self.client.post(\n", + " f\"/chat?user_id={user_id}\",\n", + " headers={\n", + " \"accept\": \"application/json\",\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " # Name is the displayed name of the request.\n", + " name=f\"/chat?user_message={request.json()}\",\n", + " data=request.json(),\n", + " catch_response=True,\n", + " ) as candidate_response:\n", + " text_response = Message.model_validate(\n", + " candidate_response.json().get(\"response\")\n", + " )\n", + "\n", + " if response is not None:\n", + " if callable(response):\n", + " error_message = response(text_response)\n", + " if error_message is not None:\n", + " candidate_response.failure(error_message)\n", + " elif text_response != response:\n", + " candidate_response.failure(\n", + " f\"Expected: {response.model_dump_json()}\\n\"\n", + " f\"Got: {text_response.model_dump_json()}\"\n", + " )\n", + "\n", + " time.sleep(self.wait_time())\n", + "\n", + " @task(3) # <- this task is 3 times more likely than the other\n", + " def dialog_1(self):\n", + " self.check_happy_path(HAPPY_PATH)\n", + "\n", + " @task\n", + " def dialog_2(self):\n", + " def check_first_message(msg: Message) -> str | None:\n", + " if msg.text is None:\n", + " return f\"Message does not contain text: {msg.model_dump_json()}\"\n", + " if \"Hi\" not in msg.text:\n", + " return (\n", + " f'\"Hi\" is not in the response message: '\n", + " f\"{msg.model_dump_json()}\"\n", + " )\n", + " return None\n", + "\n", + " self.check_happy_path(\n", + " [\n", + " # a function can be used to check the return message\n", + " (Message(text=\"Hi\"), check_first_message),\n", + " # a None is used if return message should not be checked\n", + " (Message(text=\"i'm fine, how are you?\"), None),\n", + " # this should fail\n", + " (Message(text=\"Hi\"), check_first_message),\n", + " ]\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "877ff904", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:50:58.825863Z", + "iopub.status.busy": "2023-12-27T16:50:58.825443Z", + "iopub.status.idle": "2023-12-27T16:50:58.861309Z", + "shell.execute_reply": "2023-12-27T16:50:58.844717Z" + } + }, + "outputs": [], + "source": [ + "if __name__ == \"__main__\":\n", + " if is_interactive_mode():\n", + " sys.argv = [\"locust\", \"-f\", __file__]\n", + " main.main()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.ipynb new file mode 100644 index 0000000000..c2d1cec343 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.ipynb @@ -0,0 +1,441 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "da642dbc", + "metadata": {}, + "source": [ + "# Web API: 4. Streamlit chat interface\n", + "\n", + "This tutorial shows how to use an API endpoint created in the FastAPI tutorial\n", + "in a Streamlit chat.\n", + "\n", + "A demonstration of the chat:\n", + "![demo](https://user-images.githubusercontent.com/61429541/238721597-ef88261d-e9e6-497d-ba68-0bcc9a765808.png)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0bcbb2e5", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:00.582660Z", + "iopub.status.busy": "2023-12-27T16:51:00.582465Z", + "iopub.status.idle": "2023-12-27T16:51:03.092223Z", + "shell.execute_reply": "2023-12-27T16:51:03.091179Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff streamlit streamlit-chat" + ] + }, + { + "cell_type": "markdown", + "id": "4b614e05", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## Running Streamlit:\n", + "\n", + "```bash\n", + "streamlit run {file_name}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "212331f6", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## Module and package import" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "48fe3037", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:03.095940Z", + "iopub.status.busy": "2023-12-27T16:51:03.095360Z", + "iopub.status.idle": "2023-12-27T16:51:03.100431Z", + "shell.execute_reply": "2023-12-27T16:51:03.099701Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "###########################################################\n", + "# This patch is only needed to import Message from dff.\n", + "# Streamlit Chat interface can be written without using it.\n", + "import asyncio\n", + "\n", + "loop = asyncio.new_event_loop()\n", + "asyncio.set_event_loop(loop)\n", + "###########################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02fa176f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:03.103673Z", + "iopub.status.busy": "2023-12-27T16:51:03.103187Z", + "iopub.status.idle": "2023-12-27T16:51:04.191108Z", + "shell.execute_reply": "2023-12-27T16:51:04.190429Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import uuid\n", + "import itertools\n", + "\n", + "import requests\n", + "import streamlit as st\n", + "from streamlit_chat import message\n", + "import streamlit.components.v1 as components\n", + "from dff.script import Message" + ] + }, + { + "cell_type": "markdown", + "id": "b431f4ae", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## API configuration\n", + "\n", + "Here we define methods to contact the API endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "83f2c69c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.194544Z", + "iopub.status.busy": "2023-12-27T16:51:04.193993Z", + "iopub.status.idle": "2023-12-27T16:51:04.197915Z", + "shell.execute_reply": "2023-12-27T16:51:04.197294Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "API_URL = \"http://localhost:8000/chat\"\n", + "\n", + "\n", + "def query(payload, user_id) -> requests.Response:\n", + " response = requests.post(\n", + " API_URL + f\"?user_id={user_id}\",\n", + " headers={\n", + " \"accept\": \"application/json\",\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json=payload,\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "id": "169f79cd", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## Streamlit configuration\n", + "\n", + "Here we configure Streamlit page and initialize some session variables:\n", + "\n", + "1. `user_id` -- stores user_id to be used in pipeline.\n", + "2. `bot_responses` -- a list of bot responses.\n", + "3. `user_requests` -- a list of user requests." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ad6ec305", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.200677Z", + "iopub.status.busy": "2023-12-27T16:51:04.200299Z", + "iopub.status.idle": "2023-12-27T16:51:04.223220Z", + "shell.execute_reply": "2023-12-27T16:51:04.222625Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-27 16:51:04.219 \n", + " \u001b[33m\u001b[1mWarning:\u001b[0m to view this Streamlit app on a browser, run it with the following\n", + " command:\n", + "\n", + " streamlit run /home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/ipykernel_launcher.py [ARGUMENTS]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-27 16:51:04.220 Session state does not function when running a script without `streamlit run`\n" + ] + } + ], + "source": [ + "st.set_page_config(page_title=\"Streamlit DFF Chat\", page_icon=\":robot:\")\n", + "\n", + "st.header(\"Streamlit DFF Chat\")\n", + "\n", + "if \"user_id\" not in st.session_state:\n", + " st.session_state[\"user_id\"] = str(uuid.uuid4())\n", + "\n", + "if \"bot_responses\" not in st.session_state:\n", + " st.session_state[\"bot_responses\"] = []\n", + "\n", + "if \"user_requests\" not in st.session_state:\n", + " st.session_state[\"user_requests\"] = []" + ] + }, + { + "cell_type": "markdown", + "id": "b2c83c72", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## UI setup\n", + "\n", + "Here we configure elements that will be used in Streamlit to interact with the API.\n", + "\n", + "First we define a text input field which a user is supposed to type his requests into.\n", + "Then we define a button that sends a query to the API, logs requests and responses,\n", + "and clears the text field." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b587df6c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.225940Z", + "iopub.status.busy": "2023-12-27T16:51:04.225449Z", + "iopub.status.idle": "2023-12-27T16:51:04.230242Z", + "shell.execute_reply": "2023-12-27T16:51:04.229690Z" + } + }, + "outputs": [], + "source": [ + "def send_and_receive():\n", + " \"\"\"\n", + " Send text inside the input field. Receive response from API endpoint.\n", + "\n", + " Add both the request and response to `user_requests` and `bot_responses`.\n", + "\n", + " We do not call this function inside the `text_input.on_change` because then\n", + " we'd call it whenever the text field loses focus\n", + " (e.g. when a browser tab is switched).\n", + " \"\"\"\n", + " user_request = st.session_state[\"input\"]\n", + "\n", + " if user_request == \"\":\n", + " return\n", + "\n", + " st.session_state[\"user_requests\"].append(user_request)\n", + "\n", + " bot_response = query(\n", + " Message(text=user_request).model_dump(),\n", + " user_id=st.session_state[\"user_id\"],\n", + " )\n", + " bot_response.raise_for_status()\n", + "\n", + " bot_message = Message.model_validate(bot_response.json()[\"response\"]).text\n", + "\n", + " # # Implementation without using Message:\n", + " # bot_response = query(\n", + " # {\"text\": user_request},\n", + " # user_id=st.session_state[\"user_id\"]\n", + " # )\n", + " # bot_response.raise_for_status()\n", + " #\n", + " # bot_message = bot_response.json()[\"response\"][\"text\"]\n", + "\n", + " st.session_state[\"bot_responses\"].append(bot_message)\n", + "\n", + " st.session_state[\"input\"] = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c0abe5e8", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.232685Z", + "iopub.status.busy": "2023-12-27T16:51:04.232320Z", + "iopub.status.idle": "2023-12-27T16:51:04.239089Z", + "shell.execute_reply": "2023-12-27T16:51:04.238491Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "st.text_input(\"You: \", key=\"input\")\n", + "st.button(\"Send\", on_click=send_and_receive)" + ] + }, + { + "cell_type": "markdown", + "id": "ab2bdc7b", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "### Component patch\n", + "\n", + "Here we add a component that presses the `Send` button whenever user presses the `Enter` key." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "df9ae5cd", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.241741Z", + "iopub.status.busy": "2023-12-27T16:51:04.241303Z", + "iopub.status.idle": "2023-12-27T16:51:04.246954Z", + "shell.execute_reply": "2023-12-27T16:51:04.246325Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "data": { + "text/plain": [ + "DeltaGenerator()" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "components.html(\n", + " \"\"\"\n", + "\n", + "\"\"\",\n", + " height=0,\n", + " width=0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "37ef812e", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "### Message display\n", + "\n", + "Here we use the `streamlit-chat` package to display user requests and bot responses." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d2bcc3b8", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:04.249720Z", + "iopub.status.busy": "2023-12-27T16:51:04.249262Z", + "iopub.status.idle": "2023-12-27T16:51:04.253121Z", + "shell.execute_reply": "2023-12-27T16:51:04.252536Z" + } + }, + "outputs": [], + "source": [ + "for i, bot_response, user_request in zip(\n", + " itertools.count(0),\n", + " st.session_state.get(\"bot_responses\", []),\n", + " st.session_state.get(\"user_requests\", []),\n", + "):\n", + " message(user_request, key=f\"{i}_user\", is_user=True)\n", + " message(bot_response, key=f\"{i}_bot\")" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.1_basics.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.1_basics.ipynb new file mode 100644 index 0000000000..9f71605b7d --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.1_basics.ipynb @@ -0,0 +1,226 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "db9eea73", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 1. Basics\n", + "\n", + "The following tutorial shows basic usage of `pipeline`\n", + "module as an extension to `dff.script.core`.\n", + "\n", + "Here, `__call__` (same as [run](../apiref/dff.pipeline.pipeline.pipeline.rst#dff.pipeline.pipeline.pipeline.Pipeline.run))\n", + "method is used to execute pipeline once." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b0da7fcf", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:06.383171Z", + "iopub.status.busy": "2023-12-27T16:51:06.382972Z", + "iopub.status.idle": "2023-12-27T16:51:08.705388Z", + "shell.execute_reply": "2023-12-27T16:51:08.704594Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e4d432bb", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:08.708581Z", + "iopub.status.busy": "2023-12-27T16:51:08.708157Z", + "iopub.status.idle": "2023-12-27T16:51:09.560925Z", + "shell.execute_reply": "2023-12-27T16:51:09.559944Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from dff.script import Context, Message\n", + "\n", + "from dff.pipeline import Pipeline\n", + "\n", + "from dff.utils.testing import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " HAPPY_PATH,\n", + " TOY_SCRIPT,\n", + " TOY_SCRIPT_ARGS,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1bfabe1d", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "`Pipeline` is an object, that automates script execution and context management.\n", + "`from_script` method can be used to create\n", + "a pipeline of the most basic structure:\n", + "\"preprocessors -> actor -> postprocessors\"\n", + "as well as to define `context_storage` and `messenger_interface`.\n", + "Actor is a component of :py:class:`.Pipeline`, that contains the :py:class:`.Script`\n", + "and handles it. It is responsible for processing user input and determining\n", + "the appropriate response based on the current state of the conversation and the script.\n", + "These parameters usage will be shown in tutorials 2, 3 and 6.\n", + "\n", + "Here only required parameters are provided to pipeline.\n", + "`context_storage` will default to simple Python dict and\n", + "`messenger_interface` will never be used.\n", + "pre- and postprocessors lists are empty.\n", + "`Pipeline` object can be called with user input\n", + "as first argument and dialog id (any immutable object).\n", + "This call will return `Context`,\n", + "its `last_response` property will be actors response." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b8cd99a4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:09.567354Z", + "iopub.status.busy": "2023-12-27T16:51:09.565707Z", + "iopub.status.idle": "2023-12-27T16:51:09.575714Z", + "shell.execute_reply": "2023-12-27T16:51:09.574863Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "pipeline = Pipeline.from_script(\n", + " TOY_SCRIPT,\n", + " # Pipeline script object, defined in `dff.utils.testing.toy_script`\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4135aa3f", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "For the sake of brevity, other tutorials might use `TOY_SCRIPT_ARGS` to initialize pipeline:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f5a5d7b1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:09.579615Z", + "iopub.status.busy": "2023-12-27T16:51:09.579113Z", + "iopub.status.idle": "2023-12-27T16:51:09.583169Z", + "shell.execute_reply": "2023-12-27T16:51:09.582382Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "assert TOY_SCRIPT_ARGS == (\n", + " TOY_SCRIPT,\n", + " (\"greeting_flow\", \"start_node\"),\n", + " (\"greeting_flow\", \"fallback_node\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "381728e0", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:09.586818Z", + "iopub.status.busy": "2023-12-27T16:51:09.586337Z", + "iopub.status.idle": "2023-12-27T16:51:09.598585Z", + "shell.execute_reply": "2023-12-27T16:51:09.597653Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " # a function for automatic tutorial running (testing) with HAPPY_PATH\n", + "\n", + " # This runs tutorial in interactive mode if not in IPython env\n", + " # and if `DISABLE_INTERACTIVE_MODE` is not set\n", + " if is_interactive_mode():\n", + " ctx_id = 0 # 0 will be current dialog (context) identification.\n", + " while True:\n", + " message = Message(text=input(\"Send request: \"))\n", + " ctx: Context = pipeline(message, ctx_id)\n", + " print(ctx.last_response)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.2_pre_and_post_processors.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.2_pre_and_post_processors.ipynb new file mode 100644 index 0000000000..77f50d49d1 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.2_pre_and_post_processors.ipynb @@ -0,0 +1,216 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "31543562", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 2. Pre- and postprocessors\n", + "\n", + "The following tutorial shows more advanced usage of `pipeline`\n", + "module as an extension to `dff.script.core`.\n", + "\n", + "Here, [misc](../apiref/dff.script.core.context.rst#dff.script.core.context.Context.misc)\n", + "dictionary of context is used for storing additional data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "49c9eea7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:11.280538Z", + "iopub.status.busy": "2023-12-27T16:51:11.279872Z", + "iopub.status.idle": "2023-12-27T16:51:13.633035Z", + "shell.execute_reply": "2023-12-27T16:51:13.632219Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7919bdd5", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:13.636153Z", + "iopub.status.busy": "2023-12-27T16:51:13.635685Z", + "iopub.status.idle": "2023-12-27T16:51:14.369585Z", + "shell.execute_reply": "2023-12-27T16:51:14.368901Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "from dff.messengers.common import CLIMessengerInterface\n", + "from dff.script import Context, Message\n", + "\n", + "from dff.pipeline import Pipeline\n", + "\n", + "from dff.utils.testing import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " HAPPY_PATH,\n", + " TOY_SCRIPT_ARGS,\n", + ")\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "2f111735", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "When Pipeline is created with `from_script` method, additional pre-\n", + "and postprocessors can be defined.\n", + "These can be any `ServiceBuilder` objects (defined in `types` module)\n", + "- callables, objects or dicts.\n", + "They are being turned into special `Service` objects (see tutorial 3),\n", + "that will be run before or after `Actor` respectively.\n", + "These services can be used to access external APIs, annotate user input, etc.\n", + "\n", + "Service callable signature can be one of the following:\n", + "`[ctx]`, `[ctx, pipeline]` or `[ctx, actor, info]` (see tutorial 3),\n", + "where:\n", + "\n", + "* `ctx` - Context of the current dialog.\n", + "* `pipeline` - The current pipeline.\n", + "* `info` - dictionary, containing information about\n", + " current service and pipeline execution state (see tutorial 4).\n", + "\n", + "Here a preprocessor (\"ping\") and a postprocessor (\"pong\") are added to pipeline.\n", + "They share data in `context.misc` -\n", + "a common place for sharing data between services and actor." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c7d2ea68", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:14.372785Z", + "iopub.status.busy": "2023-12-27T16:51:14.372324Z", + "iopub.status.idle": "2023-12-27T16:51:14.376176Z", + "shell.execute_reply": "2023-12-27T16:51:14.375521Z" + } + }, + "outputs": [], + "source": [ + "def ping_processor(ctx: Context):\n", + " ctx.misc[\"ping\"] = True\n", + "\n", + "\n", + "def pong_processor(ctx: Context):\n", + " ping = ctx.misc.get(\"ping\", False)\n", + " ctx.misc[\"pong\"] = ping" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "280fce52", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:14.378551Z", + "iopub.status.busy": "2023-12-27T16:51:14.378338Z", + "iopub.status.idle": "2023-12-27T16:51:14.391324Z", + "shell.execute_reply": "2023-12-27T16:51:14.390764Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " *TOY_SCRIPT_ARGS,\n", + " context_storage={}, # `context_storage` - a dictionary or\n", + " # a `DBContextStorage` instance,\n", + " # a place to store dialog contexts\n", + " messenger_interface=CLIMessengerInterface(),\n", + " # `messenger_interface` - a message channel adapter,\n", + " # it's not used in this tutorial\n", + " pre_services=[ping_processor],\n", + " post_services=[pong_processor],\n", + ")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " ctx_id = 0 # 0 will be current dialog (context) identification.\n", + " while True:\n", + " message = Message(text=input(\"Send request: \"))\n", + " ctx: Context = pipeline(message, ctx_id)\n", + " print(f\"Response: {ctx.last_response}\")\n", + " ping_pong = ctx.misc.get(\"ping\", False) and ctx.misc.get(\n", + " \"pong\", False\n", + " )\n", + " print(\n", + " f\"Ping-pong exchange: {'completed' if ping_pong else 'failed'}.\"\n", + " )\n", + " logger.info(f\"Context misc: {ctx.misc}\")" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.ipynb new file mode 100644 index 0000000000..e5c869d0eb --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.ipynb @@ -0,0 +1,232 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f700f3a8", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 3. Pipeline dict with services (basic)\n", + "\n", + "The following tutorial shows `pipeline` creation from\n", + "dict and most important pipeline components.\n", + "\n", + "Here, [Service](../apiref/dff.pipeline.service.service.rst#dff.pipeline.service.service.Service)\n", + "class, that can be used for pre- and postprocessing of messages is shown.\n", + "\n", + "Pipeline's [from_dict](../apiref/dff.pipeline.pipeline.pipeline.rst#dff.pipeline.pipeline.pipeline.Pipeline.from_dict)\n", + "static method is used for pipeline creation (from dictionary)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6db9e06d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:16.359431Z", + "iopub.status.busy": "2023-12-27T16:51:16.359218Z", + "iopub.status.idle": "2023-12-27T16:51:18.692148Z", + "shell.execute_reply": "2023-12-27T16:51:18.691350Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "66bfceb3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:18.695215Z", + "iopub.status.busy": "2023-12-27T16:51:18.694934Z", + "iopub.status.idle": "2023-12-27T16:51:19.421378Z", + "shell.execute_reply": "2023-12-27T16:51:19.420624Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "from dff.pipeline import Service, Pipeline, ACTOR\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "2c5e8a00", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "When Pipeline is created using `from_dict` method,\n", + "pipeline should be defined as a dictionary.\n", + "It should contain `services` - a `ServiceGroupBuilder` object,\n", + "basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects,\n", + "see tutorial 4.\n", + "\n", + "On pipeline execution services from `services`\n", + "list are run without difference between pre- and postprocessors.\n", + "Actor constant \"ACTOR\" should also be present among services.\n", + "ServiceBuilder object can be defined either with callable\n", + "(see tutorial 2) or with dict / object.\n", + "It should contain `handler` - a ServiceBuilder object.\n", + "\n", + "Not only Pipeline can be run using `__call__` method,\n", + "for most cases `run` method should be used.\n", + "It starts pipeline asynchronously and connects to provided messenger interface.\n", + "\n", + "Here pipeline contains 4 services,\n", + "defined in 4 different ways with different signatures." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c05b9879", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:19.425523Z", + "iopub.status.busy": "2023-12-27T16:51:19.424754Z", + "iopub.status.idle": "2023-12-27T16:51:19.429814Z", + "shell.execute_reply": "2023-12-27T16:51:19.429017Z" + } + }, + "outputs": [], + "source": [ + "def prepreprocess(_):\n", + " logger.info(\n", + " \"preprocession intent-detection Service running (defined as a dict)\"\n", + " )\n", + "\n", + "\n", + "def preprocess(_):\n", + " logger.info(\n", + " \"another preprocession web-based annotator Service \"\n", + " \"(defined as a callable)\"\n", + " )\n", + "\n", + "\n", + "def postprocess(_):\n", + " logger.info(\"postprocession Service (defined as an object)\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "deac1754", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:19.433263Z", + "iopub.status.busy": "2023-12-27T16:51:19.432674Z", + "iopub.status.idle": "2023-12-27T16:51:19.437384Z", + "shell.execute_reply": "2023-12-27T16:51:19.436744Z" + } + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " {\n", + " \"handler\": prepreprocess,\n", + " },\n", + " preprocess,\n", + " ACTOR,\n", + " Service(\n", + " handler=postprocess,\n", + " ),\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1657d594", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:19.441071Z", + "iopub.status.busy": "2023-12-27T16:51:19.440474Z", + "iopub.status.idle": "2023-12-27T16:51:19.452517Z", + "shell.execute_reply": "2023-12-27T16:51:19.451867Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline) # This runs tutorial in interactive mode" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.ipynb new file mode 100644 index 0000000000..93a6fe912e --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4d0c6d93", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 3. Pipeline dict with services (full)\n", + "\n", + "The following tutorial shows `pipeline` creation from dict\n", + "and most important pipeline components.\n", + "\n", + "This tutorial is a more advanced version of the\n", + "[previous tutorial](../tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.py)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a2333f82", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:21.371663Z", + "iopub.status.busy": "2023-12-27T16:51:21.371395Z", + "iopub.status.idle": "2023-12-27T16:51:23.688307Z", + "shell.execute_reply": "2023-12-27T16:51:23.687454Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "34ce4843", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:23.691282Z", + "iopub.status.busy": "2023-12-27T16:51:23.690811Z", + "iopub.status.idle": "2023-12-27T16:51:24.429543Z", + "shell.execute_reply": "2023-12-27T16:51:24.428908Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import json\n", + "import logging\n", + "import urllib.request\n", + "\n", + "from dff.script import Context\n", + "from dff.messengers.common import CLIMessengerInterface\n", + "from dff.pipeline import Service, Pipeline, ServiceRuntimeInfo, ACTOR\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "343df517", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "When Pipeline is created using `from_dict` method,\n", + "pipeline should be defined as `PipelineBuilder` objects\n", + "(defined in `types` module).\n", + "These objects are dictionaries of particular structure:\n", + "\n", + "* `messenger_interface` - `MessengerInterface` instance,\n", + " is used to connect to channel and transfer IO to user.\n", + "* `context_storage` - Place to store dialog contexts\n", + " (dictionary or a `DBContextStorage` instance).\n", + "* `services` (required) - A `ServiceGroupBuilder` object,\n", + " basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects,\n", + " see tutorial 4.\n", + "* `wrappers` - A list of pipeline wrappers, see tutorial 7.\n", + "* `timeout` - Pipeline timeout, see tutorial 5.\n", + "* `optimization_warnings` - Whether pipeline asynchronous structure\n", + " should be checked during initialization,\n", + " see tutorial 5.\n", + "\n", + "On pipeline execution services from `services` list are run\n", + "without difference between pre- and postprocessors.\n", + "If \"ACTOR\" constant is not found among `services` pipeline creation fails.\n", + "There can be only one \"ACTOR\" constant in the pipeline.\n", + "ServiceBuilder object can be defined either with callable (see tutorial 2) or\n", + "with dict of structure / object with following constructor arguments:\n", + "\n", + "* `handler` (required) - ServiceBuilder,\n", + " if handler is an object or a dict itself,\n", + " it will be used instead of base ServiceBuilder.\n", + " NB! Fields of nested ServiceBuilder will be overridden\n", + " by defined fields of the base ServiceBuilder.\n", + "* `wrappers` - a list of service wrappers, see tutorial 7.\n", + "* `timeout` - service timeout, see tutorial 5.\n", + "* `asynchronous` - whether or not this service _should_ be asynchronous\n", + " (keep in mind that not all services _can_ be asynchronous),\n", + " see tutorial 5.\n", + "* `start_condition` - service start condition, see tutorial 4.\n", + "* `name` - custom defined name for the service\n", + " (keep in mind that names in one ServiceGroup should be unique),\n", + " see tutorial 4.\n", + "\n", + "Not only Pipeline can be run using `__call__` method,\n", + "for most cases `run` method should be used.\n", + "It starts pipeline asynchronously and connects to provided messenger interface.\n", + "\n", + "Here pipeline contains 4 services,\n", + "defined in 4 different ways with different signatures.\n", + "First two of them write sample feature detection data to `ctx.misc`.\n", + "The first uses a constant expression and the second fetches from `example.com`.\n", + "Third one is \"ACTOR\" constant (it acts like a _special_ service here).\n", + "Final service logs `ctx.misc` dict." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "83879b4d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:24.432773Z", + "iopub.status.busy": "2023-12-27T16:51:24.432345Z", + "iopub.status.idle": "2023-12-27T16:51:24.438791Z", + "shell.execute_reply": "2023-12-27T16:51:24.438145Z" + } + }, + "outputs": [], + "source": [ + "def prepreprocess(ctx: Context):\n", + " logger.info(\n", + " \"preprocession intent-detection Service running (defined as a dict)\"\n", + " )\n", + " ctx.misc[\"preprocess_detection\"] = {\n", + " ctx.last_request.text: \"some_intent\"\n", + " } # Similar syntax can be used to access\n", + " # service output dedicated to current pipeline run\n", + "\n", + "\n", + "def preprocess(ctx: Context, _, info: ServiceRuntimeInfo):\n", + " logger.info(\n", + " f\"another preprocession web-based annotator Service\"\n", + " f\"(defined as a callable), named '{info.name}'\"\n", + " )\n", + " with urllib.request.urlopen(\"https://example.com/\") as webpage:\n", + " web_content = webpage.read().decode(\n", + " webpage.headers.get_content_charset()\n", + " )\n", + " ctx.misc[\"another_detection\"] = {\n", + " ctx.last_request.text: \"online\"\n", + " if \"Example Domain\" in web_content\n", + " else \"offline\"\n", + " }\n", + "\n", + "\n", + "def postprocess(ctx: Context, pl: Pipeline):\n", + " logger.info(\"postprocession Service (defined as an object)\")\n", + " logger.info(\n", + " f\"resulting misc looks like:\"\n", + " f\"{json.dumps(ctx.misc, indent=4, default=str)}\"\n", + " )\n", + " fallback_flow, fallback_node, _ = pl.actor.fallback_label\n", + " received_response = pl.script[fallback_flow][fallback_node].response\n", + " responses_match = received_response == ctx.last_response\n", + " logger.info(f\"actor is{'' if responses_match else ' not'} in fallback node\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5082df01", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:24.441407Z", + "iopub.status.busy": "2023-12-27T16:51:24.441028Z", + "iopub.status.idle": "2023-12-27T16:51:24.446594Z", + "shell.execute_reply": "2023-12-27T16:51:24.445868Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"messenger_interface\": CLIMessengerInterface(\n", + " intro=\"Hi, this is a brand new Pipeline running!\",\n", + " prompt_request=\"Request: \",\n", + " prompt_response=\"Response: \",\n", + " ), # `CLIMessengerInterface` has the following constructor parameters:\n", + " # `intro` - a string that will be displayed\n", + " # on connection to interface (on `pipeline.run`)\n", + " # `prompt_request` - a string that will be displayed before user input\n", + " # `prompt_response` - an output prefix string\n", + " \"context_storage\": {},\n", + " \"components\": [\n", + " {\n", + " \"handler\": {\n", + " \"handler\": prepreprocess,\n", + " \"name\": \"silly_service_name\",\n", + " },\n", + " \"name\": \"preprocessor\",\n", + " }, # This service will be named `preprocessor`\n", + " # handler name will be overridden\n", + " preprocess,\n", + " ACTOR,\n", + " Service(\n", + " handler=postprocess,\n", + " name=\"postprocessor\",\n", + " ),\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "28c6c1fd", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:24.449964Z", + "iopub.status.busy": "2023-12-27T16:51:24.449510Z", + "iopub.status.idle": "2023-12-27T16:51:24.531246Z", + "shell.execute_reply": "2023-12-27T16:51:24.530474Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.ipynb new file mode 100644 index 0000000000..9b964974d5 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.ipynb @@ -0,0 +1,260 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cfa38c72", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 4. Groups and conditions (basic)\n", + "\n", + "The following example shows `pipeline` service group usage and start conditions.\n", + "\n", + "Here, [Service](../apiref/dff.pipeline.service.service.rst#dff.pipeline.service.service.Service)s\n", + "and [ServiceGroup](../apiref/dff.pipeline.service.group.rst#dff.pipeline.service.group.ServiceGroup)s\n", + "are shown for advanced data pre- and postprocessing based on conditions." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9f41b26c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:26.378089Z", + "iopub.status.busy": "2023-12-27T16:51:26.377431Z", + "iopub.status.idle": "2023-12-27T16:51:28.738012Z", + "shell.execute_reply": "2023-12-27T16:51:28.737181Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2e8af2bf", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:28.741048Z", + "iopub.status.busy": "2023-12-27T16:51:28.740625Z", + "iopub.status.idle": "2023-12-27T16:51:29.474595Z", + "shell.execute_reply": "2023-12-27T16:51:29.473992Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import json\n", + "import logging\n", + "\n", + "from dff.pipeline import (\n", + " Service,\n", + " Pipeline,\n", + " not_condition,\n", + " service_successful_condition,\n", + " ServiceRuntimeInfo,\n", + " ACTOR,\n", + ")\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "a93d28db", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Pipeline can contain not only single services, but also service groups.\n", + "Service groups can be defined as `ServiceGroupBuilder` objects:\n", + " lists of `ServiceBuilders` and `ServiceGroupBuilders` or objects.\n", + "The objects should contain `services` -\n", + "a ServiceBuilder and ServiceGroupBuilder object list.\n", + "\n", + "To receive serialized information about service,\n", + " service group or pipeline a property `info_dict` can be used,\n", + " it returns important object properties as a dict.\n", + "\n", + "Services and service groups can be executed conditionally.\n", + "Conditions are functions passed to `start_condition` argument.\n", + "These functions should have following signature:\n", + "\n", + " (ctx: Context, pipeline: Pipeline) -> bool.\n", + "\n", + "Service is only executed if its start_condition returned `True`.\n", + "By default all the services start unconditionally.\n", + "There are number of built-in condition functions.\n", + "Built-in condition functions check other service states.\n", + "These are most important built-in condition functions:\n", + "\n", + "* `always_start_condition` - Default condition function, always starts service.\n", + "* `service_successful_condition(path)` - Function that checks,\n", + " whether service with given `path` executed successfully.\n", + "* `not_condition(function)` - Function that returns result\n", + " opposite from the one returned\n", + " by the `function` (condition function) argument.\n", + "\n", + "Here there is a conditionally executed service named\n", + "`never_running_service` is always executed.\n", + "It is executed only if `always_running_service`\n", + "is not finished, that should never happen.\n", + "The service named `context_printing_service`\n", + "prints pipeline runtime information,\n", + "that contains execution state of all previously run services." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2983402e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:29.477903Z", + "iopub.status.busy": "2023-12-27T16:51:29.477289Z", + "iopub.status.idle": "2023-12-27T16:51:29.481630Z", + "shell.execute_reply": "2023-12-27T16:51:29.481083Z" + } + }, + "outputs": [], + "source": [ + "def always_running_service(_, __, info: ServiceRuntimeInfo):\n", + " logger.info(f\"Service '{info.name}' is running...\")\n", + "\n", + "\n", + "def never_running_service(_, __, info: ServiceRuntimeInfo):\n", + " raise Exception(f\"Oh no! The '{info.name}' service is running!\")\n", + "\n", + "\n", + "def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo):\n", + " logger.info(\n", + " f\"Service '{info.name}' runtime execution info:\"\n", + " f\"{json.dumps(info, indent=4, default=str)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d9f3d2d0", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:29.484076Z", + "iopub.status.busy": "2023-12-27T16:51:29.483870Z", + "iopub.status.idle": "2023-12-27T16:51:29.487915Z", + "shell.execute_reply": "2023-12-27T16:51:29.487329Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " Service(\n", + " handler=always_running_service,\n", + " name=\"always_running_service\",\n", + " ),\n", + " ACTOR,\n", + " Service(\n", + " handler=never_running_service,\n", + " start_condition=not_condition(\n", + " service_successful_condition(\".pipeline.always_running_service\")\n", + " ),\n", + " ),\n", + " Service(\n", + " handler=runtime_info_printing_service,\n", + " name=\"runtime_info_printing_service\",\n", + " ),\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2ca03bff", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:29.490227Z", + "iopub.status.busy": "2023-12-27T16:51:29.490024Z", + "iopub.status.idle": "2023-12-27T16:51:29.502888Z", + "shell.execute_reply": "2023-12-27T16:51:29.502321Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_full.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_full.ipynb new file mode 100644 index 0000000000..9c5f210b45 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.4_groups_and_conditions_full.ipynb @@ -0,0 +1,389 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ef24d24e", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 4. Groups and conditions (full)\n", + "\n", + "The following tutorial shows `pipeline` service group usage and start conditions.\n", + "\n", + "This tutorial is a more advanced version of the\n", + "[previous tutorial](../tutorials/tutorials.pipeline.4_groups_and_conditions_basic.py)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3b25965a", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:31.246870Z", + "iopub.status.busy": "2023-12-27T16:51:31.246240Z", + "iopub.status.idle": "2023-12-27T16:51:33.686139Z", + "shell.execute_reply": "2023-12-27T16:51:33.685329Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b13b1543", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:33.689356Z", + "iopub.status.busy": "2023-12-27T16:51:33.688930Z", + "iopub.status.idle": "2023-12-27T16:51:34.425181Z", + "shell.execute_reply": "2023-12-27T16:51:34.424567Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "from dff.pipeline import (\n", + " Service,\n", + " Pipeline,\n", + " ServiceGroup,\n", + " not_condition,\n", + " service_successful_condition,\n", + " all_condition,\n", + " ServiceRuntimeInfo,\n", + " ACTOR,\n", + ")\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "f616ae69", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Pipeline can contain not only single services, but also service groups.\n", + "Service groups can be defined as lists of `ServiceBuilders`\n", + " (in fact, all of the pipeline services are combined\n", + " into root service group named \"pipeline\").\n", + "Alternatively, the groups can be defined as objects\n", + " with following constructor arguments:\n", + "\n", + "* `components` (required) - A list of ServiceBuilder objects,\n", + " ServiceGroup objects and lists of them.\n", + "* `wrappers` - A list of pipeline wrappers, see tutorial 7.\n", + "* `timeout` - Pipeline timeout, see tutorial 5.\n", + "* `asynchronous` - Whether or not this service group _should_ be asynchronous\n", + " (keep in mind that not all service groups _can_ be asynchronous),\n", + " see tutorial 5.\n", + "* `start_condition` - Service group start condition.\n", + "* `name` - Custom defined name for the service group\n", + " (keep in mind that names in one ServiceGroup should be unique).\n", + "\n", + "Service (and service group) object fields\n", + "are mostly the same as constructor parameters,\n", + "however there are some differences:\n", + "\n", + "* `requested_async_flag` - Contains the value received\n", + " from `asynchronous` constructor parameter.\n", + "* `calculated_async_flag` - Contains automatically calculated\n", + " possibility of the service to be asynchronous.\n", + "* `asynchronous` - Combination af `..._async_flag` fields,\n", + " requested value overrides calculated (if not `None`),\n", + " see tutorial 5.\n", + "* `path` - Contains globally unique (for pipeline)\n", + " path to the service or service group.\n", + "\n", + "If no name is specified for a service or service group,\n", + " the name will be generated according to the following rules:\n", + "\n", + "1. If service's handler is an Actor, service will be named 'actor'.\n", + "2. If service's handler is callable,\n", + " service will be named callable.\n", + "3. Service group will be named 'service_group'.\n", + "4. Otherwise, it will be named 'noname_service'.\n", + "5. After that an index will be added to service name.\n", + "\n", + "To receive serialized information about service, service group\n", + "or pipeline a property `info_dict` can be used,\n", + "it returns important object properties as a dict.\n", + "In addition to that `pretty_format` method of Pipeline\n", + "can be used to get all pipeline properties as a formatted string\n", + "(e.g. for logging or debugging purposes).\n", + "\n", + "Services and service groups can be executed conditionally.\n", + "Conditions are functions passed to `start_condition` argument.\n", + "These functions should have following signature:\n", + "\n", + " (ctx: Context, pipeline: Pipeline) -> bool.\n", + "\n", + "Service is only executed if its start_condition returned `True`.\n", + "By default all the services start unconditionally.\n", + "There are number of built-in condition functions as well\n", + "as possibility to create custom ones.\n", + "Custom condition functions can rely on data in `ctx.misc`\n", + "as well as on any external data source.\n", + "Built-in condition functions check other service states.\n", + "All of the services store their execution status in context,\n", + " this status can be one of the following:\n", + "\n", + "* `NOT_RUN` - Service hasn't bee executed yet.\n", + "* `RUNNING` - Service is currently being executed\n", + " (important for asynchronous services).\n", + "* `FINISHED` - Service finished successfully.\n", + "* `FAILED` - Service execution failed (that also throws an exception).\n", + "\n", + "There are following built-in condition functions:\n", + "\n", + "* `always_start_condition` - Default condition function,\n", + " always starts service.\n", + "* `service_successful_condition(path)` -\n", + " Function that checks, whether service\n", + " with given `path` executed successfully (is `FINISHED`).\n", + "* `not_condition(function)` -\n", + " Function that returns result opposite\n", + " from the one returned by\n", + " the `function` (condition function) argument.\n", + "* `aggregate_condition(aggregator, *functions)` -\n", + " Function that aggregated results of\n", + " numerous `functions` (condition functions)\n", + " using special `aggregator` function.\n", + "* `all_condition(*functions)` -\n", + " Function that returns True only if all\n", + " of the given `functions`\n", + " (condition functions) return `True`.\n", + "* `any_condition(*functions)` -\n", + " Function that returns `True`\n", + " if any of the given `functions`\n", + " (condition functions) return `True`.\n", + "NB! Actor service ALWAYS runs unconditionally.\n", + "\n", + "Here there are two conditionally executed services:\n", + "a service named `running_service` is executed\n", + " only if both `simple_services` in `service_group_0`\n", + " are finished successfully.\n", + "`never_running_service` is executed only if `running_service` is not finished,\n", + "this should never happen.\n", + "`context_printing_service` prints pipeline runtime information,\n", + " that contains execution state of all previously run services." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "51fa2ea3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:34.428620Z", + "iopub.status.busy": "2023-12-27T16:51:34.428007Z", + "iopub.status.idle": "2023-12-27T16:51:34.432394Z", + "shell.execute_reply": "2023-12-27T16:51:34.431762Z" + } + }, + "outputs": [], + "source": [ + "def simple_service(_, __, info: ServiceRuntimeInfo):\n", + " logger.info(f\"Service '{info.name}' is running...\")\n", + "\n", + "\n", + "def never_running_service(_, __, info: ServiceRuntimeInfo):\n", + " raise Exception(f\"Oh no! The '{info.name}' service is running!\")\n", + "\n", + "\n", + "def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo):\n", + " logger.info(\n", + " f\"Service '{info.name}' runtime execution info:\"\n", + " f\"{info.model_dump_json(indent=4, default=str)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6cf04ea2", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:34.434861Z", + "iopub.status.busy": "2023-12-27T16:51:34.434530Z", + "iopub.status.idle": "2023-12-27T16:51:34.439527Z", + "shell.execute_reply": "2023-12-27T16:51:34.438972Z" + } + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " [\n", + " simple_service, # This simple service\n", + " # will be named `simple_service_0`\n", + " simple_service, # This simple service\n", + " # will be named `simple_service_1`\n", + " ], # Despite this is the unnamed service group in the root\n", + " # service group, it will be named `service_group_0`\n", + " ACTOR,\n", + " ServiceGroup(\n", + " name=\"named_group\",\n", + " components=[\n", + " Service(\n", + " handler=simple_service,\n", + " start_condition=all_condition(\n", + " service_successful_condition(\n", + " \".pipeline.service_group_0.simple_service_0\"\n", + " ),\n", + " service_successful_condition(\n", + " \".pipeline.service_group_0.simple_service_1\"\n", + " ),\n", + " ), # Alternative:\n", + " # service_successful_condition(\".pipeline.service_group_0\")\n", + " name=\"running_service\",\n", + " ), # This simple service will be named `running_service`,\n", + " # because its name is manually overridden\n", + " Service(\n", + " handler=never_running_service,\n", + " start_condition=not_condition(\n", + " service_successful_condition(\n", + " \".pipeline.named_group.running_service\"\n", + " )\n", + " ),\n", + " ),\n", + " ],\n", + " ),\n", + " runtime_info_printing_service,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "47a04f49", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:34.442200Z", + "iopub.status.busy": "2023-12-27T16:51:34.441762Z", + "iopub.status.idle": "2023-12-27T16:51:34.464363Z", + "shell.execute_reply": "2023-12-27T16:51:34.463800Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Service 'runtime_info_printing_service_0' execution failed!\n", + "model_dump_json() got an unexpected keyword argument 'default'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Service 'runtime_info_printing_service_0' execution failed!\n", + "model_dump_json() got an unexpected keyword argument 'default'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Service 'runtime_info_printing_service_0' execution failed!\n", + "model_dump_json() got an unexpected keyword argument 'default'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Service 'runtime_info_printing_service_0' execution failed!\n", + "model_dump_json() got an unexpected keyword argument 'default'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Service 'runtime_info_printing_service_0' execution failed!\n", + "model_dump_json() got an unexpected keyword argument 'default'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " logger.info(f\"Pipeline structure:\\n{pipeline.pretty_format()}\")\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.ipynb new file mode 100644 index 0000000000..f0548ac940 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cb9f5b85", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 5. Asynchronous groups and services (basic)\n", + "\n", + "The following tutorial shows `pipeline` asynchronous\n", + "service and service group usage.\n", + "\n", + "Here, [ServiceGroup](../apiref/dff.pipeline.service.group.rst#dff.pipeline.service.group.ServiceGroup)s\n", + "are shown for advanced and asynchronous data pre- and postprocessing." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "14bf07cc", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:36.367668Z", + "iopub.status.busy": "2023-12-27T16:51:36.367241Z", + "iopub.status.idle": "2023-12-27T16:51:38.783944Z", + "shell.execute_reply": "2023-12-27T16:51:38.783061Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7e95ab44", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:38.786843Z", + "iopub.status.busy": "2023-12-27T16:51:38.786633Z", + "iopub.status.idle": "2023-12-27T16:51:39.517636Z", + "shell.execute_reply": "2023-12-27T16:51:39.517039Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "\n", + "from dff.pipeline import Pipeline, ACTOR\n", + "\n", + "from dff.utils.testing.common import (\n", + " is_interactive_mode,\n", + " check_happy_path,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT" + ] + }, + { + "cell_type": "markdown", + "id": "f05e7d68", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "Services and service groups can be synchronous and asynchronous.\n", + "In synchronous service groups services are executed consequently.\n", + "In asynchronous service groups all services are executed simultaneously.\n", + "\n", + "Service can be asynchronous if its handler is an async function.\n", + "Service group can be asynchronous if all services\n", + "and service groups inside it are asynchronous.\n", + "\n", + "Here there is an asynchronous service group, that contains 10 services,\n", + "each of them should sleep for 0.01 of a second.\n", + "However, as the group is asynchronous,\n", + "it is being executed for 0.01 of a second in total.\n", + "Service group `pipeline` can't be asynchronous because `actor` is synchronous." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "00bc6768", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:39.521002Z", + "iopub.status.busy": "2023-12-27T16:51:39.520445Z", + "iopub.status.idle": "2023-12-27T16:51:39.524430Z", + "shell.execute_reply": "2023-12-27T16:51:39.523790Z" + } + }, + "outputs": [], + "source": [ + "async def time_consuming_service(_):\n", + " await asyncio.sleep(0.01)\n", + "\n", + "\n", + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " [time_consuming_service for _ in range(0, 10)],\n", + " ACTOR,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f5932638", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:39.527052Z", + "iopub.status.busy": "2023-12-27T16:51:39.526595Z", + "iopub.status.idle": "2023-12-27T16:51:39.597327Z", + "shell.execute_reply": "2023-12-27T16:51:39.596599Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.ipynb new file mode 100644 index 0000000000..0947384208 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.ipynb @@ -0,0 +1,313 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "62ba5d02", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 5. Asynchronous groups and services (full)\n", + "\n", + "The following tutorial shows `pipeline`\n", + "asynchronous service and service group usage.\n", + "\n", + "This tutorial is a more advanced version of the\n", + "[previous tutorial](../tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.py)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "710342da", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:41.425774Z", + "iopub.status.busy": "2023-12-27T16:51:41.425208Z", + "iopub.status.idle": "2023-12-27T16:51:43.742847Z", + "shell.execute_reply": "2023-12-27T16:51:43.741802Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0a6524a0", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:43.745957Z", + "iopub.status.busy": "2023-12-27T16:51:43.745715Z", + "iopub.status.idle": "2023-12-27T16:51:44.625917Z", + "shell.execute_reply": "2023-12-27T16:51:44.625226Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "import json\n", + "import logging\n", + "import urllib.request\n", + "\n", + "from dff.script import Context\n", + "\n", + "from dff.pipeline import ServiceGroup, Pipeline, ServiceRuntimeInfo, ACTOR\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "38daa1c4", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "Services and service groups can be synchronous and asynchronous.\n", + "In synchronous service groups services are executed consequently,\n", + " some of them (`ACTOR`) can even return `Context` object,\n", + " modifying it.\n", + "In asynchronous service groups all services\n", + " are executed simultaneously and should not return anything,\n", + " neither modify Context.\n", + "\n", + "To become asynchronous service or service group\n", + " should _be able_ to be asynchronous\n", + " and should not be marked synchronous.\n", + "Service can be asynchronous if its handler is an async function.\n", + "Service group can be asynchronous if all services\n", + "and service groups inside it are asynchronous.\n", + "If service or service group can be asynchronous\n", + "the `asynchronous` constructor parameter is checked.\n", + "If the parameter is not set,\n", + "the service becomes asynchronous, and if set, it is used instead.\n", + "If service can not be asynchronous,\n", + "but is marked asynchronous, an exception is thrown.\n", + "NB! ACTOR service is always synchronous.\n", + "\n", + "The timeout field only works for asynchronous services and service groups.\n", + "If service execution takes more time than timeout,\n", + "it is aborted and marked as failed.\n", + "\n", + "Pipeline `optimization_warnings` argument can be used to\n", + " display optimization warnings during pipeline construction.\n", + "Generally for optimization purposes asynchronous\n", + " services should be combined into asynchronous\n", + " groups to run simultaneously.\n", + "Synchronous services should be expelled from (mostly) asynchronous groups.\n", + "\n", + "Here service group `balanced_group` can be asynchronous,\n", + " however it is requested to be synchronous,\n", + " so its services are executed consequently.\n", + "Service group `service_group_0` is asynchronous,\n", + " it doesn't run out of timeout of 0.02 seconds,\n", + " however contains 6 time consuming services,\n", + " each of them sleeps for 0.01 of a second.\n", + "Service group `service_group_1` is also asynchronous,\n", + "it logs HTTPS requests (from 1 to 15),\n", + " running simultaneously, in random order.\n", + "Service group `pipeline` can't be asynchronous because\n", + "`balanced_group` and ACTOR are synchronous." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3baf85a3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:44.629502Z", + "iopub.status.busy": "2023-12-27T16:51:44.628876Z", + "iopub.status.idle": "2023-12-27T16:51:44.635146Z", + "shell.execute_reply": "2023-12-27T16:51:44.634554Z" + } + }, + "outputs": [], + "source": [ + "async def simple_asynchronous_service(_, __, info: ServiceRuntimeInfo):\n", + " logger.info(f\"Service '{info.name}' is running\")\n", + "\n", + "\n", + "async def time_consuming_service(_):\n", + " await asyncio.sleep(0.01)\n", + "\n", + "\n", + "def meta_web_querying_service(\n", + " photo_number: int,\n", + "): # This function returns services, a service factory\n", + " async def web_querying_service(ctx: Context, _, info: ServiceRuntimeInfo):\n", + " if ctx.misc.get(\"web_query\", None) is None:\n", + " ctx.misc[\"web_query\"] = {}\n", + " with urllib.request.urlopen(\n", + " f\"https://jsonplaceholder.typicode.com/photos/{photo_number}\"\n", + " ) as webpage:\n", + " web_content = webpage.read().decode(\n", + " webpage.headers.get_content_charset()\n", + " )\n", + " ctx.misc[\"web_query\"].update(\n", + " {\n", + " f\"{ctx.last_request}\"\n", + " f\":photo_number_{photo_number}\": json.loads(web_content)[\n", + " \"title\"\n", + " ]\n", + " }\n", + " )\n", + " logger.info(f\"Service '{info.name}' has completed HTTPS request\")\n", + "\n", + " return web_querying_service\n", + "\n", + "\n", + "def context_printing_service(ctx: Context):\n", + " logger.info(f\"Context misc: {json.dumps(ctx.misc, indent=4, default=str)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "30397ffb", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:44.637897Z", + "iopub.status.busy": "2023-12-27T16:51:44.637344Z", + "iopub.status.idle": "2023-12-27T16:51:44.642573Z", + "shell.execute_reply": "2023-12-27T16:51:44.641978Z" + } + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"optimization_warnings\": True,\n", + " # There are no warnings - pipeline is well-optimized\n", + " \"components\": [\n", + " ServiceGroup(\n", + " name=\"balanced_group\",\n", + " asynchronous=False,\n", + " components=[\n", + " simple_asynchronous_service,\n", + " ServiceGroup(\n", + " timeout=0.02,\n", + " components=[time_consuming_service for _ in range(0, 6)],\n", + " ),\n", + " simple_asynchronous_service,\n", + " ],\n", + " ),\n", + " ACTOR,\n", + " [meta_web_querying_service(photo) for photo in range(1, 16)],\n", + " context_printing_service,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6f8ff726", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:44.645090Z", + "iopub.status.busy": "2023-12-27T16:51:44.644706Z", + "iopub.status.idle": "2023-12-27T16:51:46.534642Z", + "shell.execute_reply": "2023-12-27T16:51:46.533954Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_basic.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_basic.ipynb new file mode 100644 index 0000000000..f932d23ba8 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_basic.ipynb @@ -0,0 +1,258 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "aeed2f40", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 6. Extra Handlers (basic)\n", + "\n", + "The following tutorial shows extra handlers possibilities and use cases.\n", + "\n", + "Here, extra handlers [BeforeHandler](../apiref/dff.pipeline.service.extra.rst#dff.pipeline.service.extra.BeforeHandler)\n", + "and [AfterHandler](../apiref/dff.pipeline.service.extra.rst#dff.pipeline.service.extra.AfterHandler)\n", + "are shown as additional means of data processing, attached to services." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4d91b1b8", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:48.351439Z", + "iopub.status.busy": "2023-12-27T16:51:48.351243Z", + "iopub.status.idle": "2023-12-27T16:51:50.926859Z", + "shell.execute_reply": "2023-12-27T16:51:50.926037Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "49a2aa33", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:50.929716Z", + "iopub.status.busy": "2023-12-27T16:51:50.929500Z", + "iopub.status.idle": "2023-12-27T16:51:51.617837Z", + "shell.execute_reply": "2023-12-27T16:51:51.617118Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "import json\n", + "import logging\n", + "import random\n", + "from datetime import datetime\n", + "\n", + "from dff.script import Context\n", + "\n", + "from dff.pipeline import Pipeline, ServiceGroup, ExtraHandlerRuntimeInfo, ACTOR\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "593c8ea5", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Extra handlers are additional function\n", + " lists (before-functions and/or after-functions)\n", + " that can be added to any `pipeline` components (service and service groups).\n", + "Extra handlers main purpose should be service\n", + "and service groups statistics collection.\n", + "Extra handlers can be attached to pipeline component using\n", + "`before_handler` and `after_handler` constructor parameter.\n", + "\n", + "Here 5 `heavy_service`s are run in single asynchronous service group.\n", + "Each of them sleeps for random amount of seconds (between 0 and 0.05).\n", + "To each of them (as well as to group)\n", + " time measurement extra handler is attached,\n", + " that writes execution time to `ctx.misc`.\n", + "In the end `ctx.misc` is logged to info channel." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4166b60d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:51.621035Z", + "iopub.status.busy": "2023-12-27T16:51:51.620675Z", + "iopub.status.idle": "2023-12-27T16:51:51.625771Z", + "shell.execute_reply": "2023-12-27T16:51:51.625135Z" + } + }, + "outputs": [], + "source": [ + "def collect_timestamp_before(ctx: Context, _, info: ExtraHandlerRuntimeInfo):\n", + " ctx.misc.update({f\"{info.component.name}\": datetime.now()})\n", + "\n", + "\n", + "def collect_timestamp_after(ctx: Context, _, info: ExtraHandlerRuntimeInfo):\n", + " ctx.misc.update(\n", + " {\n", + " f\"{info.component.name}\": datetime.now()\n", + " - ctx.misc[f\"{info.component.name}\"]\n", + " }\n", + " )\n", + "\n", + "\n", + "async def heavy_service(_):\n", + " await asyncio.sleep(random.randint(0, 5) / 100)\n", + "\n", + "\n", + "def logging_service(ctx: Context):\n", + " logger.info(f\"Context misc: {json.dumps(ctx.misc, indent=4, default=str)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bc92f033", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:51.628301Z", + "iopub.status.busy": "2023-12-27T16:51:51.627929Z", + "iopub.status.idle": "2023-12-27T16:51:51.633183Z", + "shell.execute_reply": "2023-12-27T16:51:51.632583Z" + } + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " ServiceGroup(\n", + " before_handler=[collect_timestamp_before],\n", + " after_handler=[collect_timestamp_after],\n", + " components=[\n", + " {\n", + " \"handler\": heavy_service,\n", + " \"before_handler\": [collect_timestamp_before],\n", + " \"after_handler\": [collect_timestamp_after],\n", + " },\n", + " {\n", + " \"handler\": heavy_service,\n", + " \"before_handler\": [collect_timestamp_before],\n", + " \"after_handler\": [collect_timestamp_after],\n", + " },\n", + " {\n", + " \"handler\": heavy_service,\n", + " \"before_handler\": [collect_timestamp_before],\n", + " \"after_handler\": [collect_timestamp_after],\n", + " },\n", + " {\n", + " \"handler\": heavy_service,\n", + " \"before_handler\": [collect_timestamp_before],\n", + " \"after_handler\": [collect_timestamp_after],\n", + " },\n", + " {\n", + " \"handler\": heavy_service,\n", + " \"before_handler\": [collect_timestamp_before],\n", + " \"after_handler\": [collect_timestamp_after],\n", + " },\n", + " ],\n", + " ),\n", + " ACTOR,\n", + " logging_service,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "60ba3649", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:51.635647Z", + "iopub.status.busy": "2023-12-27T16:51:51.635339Z", + "iopub.status.idle": "2023-12-27T16:51:51.882976Z", + "shell.execute_reply": "2023-12-27T16:51:51.882286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline(**pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_full.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_full.ipynb new file mode 100644 index 0000000000..2844159b90 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.6_extra_handlers_full.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c6e4201e", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 6. Extra Handlers (full)\n", + "\n", + "The following tutorial shows extra handlers possibilities and use cases.\n", + "\n", + "This tutorial is a more advanced version of the\n", + "[previous tutorial](../tutorials/tutorials.pipeline.6_extra_handlers_basic.py)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f693f3e4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:53.561889Z", + "iopub.status.busy": "2023-12-27T16:51:53.561686Z", + "iopub.status.idle": "2023-12-27T16:51:55.986855Z", + "shell.execute_reply": "2023-12-27T16:51:55.986049Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff psutil" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c972dd48", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:55.990032Z", + "iopub.status.busy": "2023-12-27T16:51:55.989529Z", + "iopub.status.idle": "2023-12-27T16:51:56.701350Z", + "shell.execute_reply": "2023-12-27T16:51:56.700703Z" + } + }, + "outputs": [], + "source": [ + "import json\n", + "import logging\n", + "import random\n", + "from datetime import datetime\n", + "\n", + "import psutil\n", + "from dff.script import Context\n", + "\n", + "from dff.pipeline import (\n", + " Pipeline,\n", + " ServiceGroup,\n", + " to_service,\n", + " ExtraHandlerRuntimeInfo,\n", + " ServiceRuntimeInfo,\n", + " ACTOR,\n", + ")\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "bff6cd98", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Extra handlers are additional function lists\n", + " (before-functions and/or after-functions)\n", + " that can be added to any pipeline components (service and service groups).\n", + "Despite extra handlers can be used to prepare data for certain services,\n", + "that require some very special input type,\n", + " in most cases services should be preferred for that purpose.\n", + "Extra handlers can be asynchronous,\n", + "however there's no statistics that can be collected about them.\n", + "So their main purpose should be _really_ lightweight data conversion (etc.)\n", + " operations or service and service groups statistics collection.\n", + "\n", + "Extra handlers have the following constructor arguments / parameters:\n", + "\n", + "* `functions` - Functions that will be run.\n", + "* `timeout` - Timeout for that extra handler\n", + " (for asynchronous extra handlers only).\n", + "* `asynchronous` - Whether this extra handler should be asynchronous or not.\n", + "NB! Extra handlers don't have execution state,\n", + "so their names shouldn't appear in built-in condition functions.\n", + "\n", + "Extra handlers callable signature can be one of the following:\n", + "`[ctx]`, `[ctx, pipeline]` or `[ctx, pipeline, info]`, where:\n", + "\n", + "* `ctx` - `Context` of the current dialog.\n", + "* `pipeline` - The current pipeline.\n", + "* `info` - Dictionary, containing information about current extra handler\n", + " and pipeline execution state (see tutorial 4).\n", + "\n", + "Extra handlers can be attached to pipeline component in a few different ways:\n", + "\n", + "1. Directly in constructor - by adding extra handlers to\n", + " `before_handler` or `after_handler` constructor parameter.\n", + "2. (Services only) `to_service` decorator -\n", + " transforms function to service with extra handlers\n", + " from `before_handler` and `after_handler` arguments.\n", + "\n", + "Here 5 `heavy_service`s fill big amounts of memory with random numbers.\n", + "Their runtime stats are captured and displayed by extra services,\n", + "`time_measure_handler` measures time and\n", + "`ram_measure_handler` - allocated memory.\n", + "Another `time_measure_handler` measures total\n", + "amount of time taken by all of them (combined in service group).\n", + "`logging_service` logs stats, however it can use string arguments only,\n", + " so `json_encoder_handler` is applied to encode stats to JSON." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "61ef485c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:56.704799Z", + "iopub.status.busy": "2023-12-27T16:51:56.704137Z", + "iopub.status.idle": "2023-12-27T16:51:56.711694Z", + "shell.execute_reply": "2023-12-27T16:51:56.711090Z" + } + }, + "outputs": [], + "source": [ + "def get_extra_handler_misc_field(\n", + " info: ExtraHandlerRuntimeInfo, postfix: str\n", + ") -> str: # This method calculates `misc` field name dedicated to extra handler\n", + " # based on its and its service name\n", + " return f\"{info.component.name}-{postfix}\"\n", + "\n", + "\n", + "def time_measure_before_handler(ctx, _, info):\n", + " ctx.misc.update(\n", + " {get_extra_handler_misc_field(info, \"time\"): datetime.now()}\n", + " )\n", + "\n", + "\n", + "def time_measure_after_handler(ctx, _, info):\n", + " ctx.misc.update(\n", + " {\n", + " get_extra_handler_misc_field(info, \"time\"): datetime.now()\n", + " - ctx.misc[get_extra_handler_misc_field(info, \"time\")]\n", + " }\n", + " )\n", + "\n", + "\n", + "def ram_measure_before_handler(ctx, _, info):\n", + " ctx.misc.update(\n", + " {\n", + " get_extra_handler_misc_field(\n", + " info, \"ram\"\n", + " ): psutil.virtual_memory().available\n", + " }\n", + " )\n", + "\n", + "\n", + "def ram_measure_after_handler(ctx, _, info):\n", + " ctx.misc.update(\n", + " {\n", + " get_extra_handler_misc_field(info, \"ram\"): ctx.misc[\n", + " get_extra_handler_misc_field(info, \"ram\")\n", + " ]\n", + " - psutil.virtual_memory().available\n", + " }\n", + " )\n", + "\n", + "\n", + "def json_converter_before_handler(ctx, _, info):\n", + " ctx.misc.update(\n", + " {\n", + " get_extra_handler_misc_field(info, \"str\"): json.dumps(\n", + " ctx.misc, indent=4, default=str\n", + " )\n", + " }\n", + " )\n", + "\n", + "\n", + "def json_converter_after_handler(ctx, _, info):\n", + " ctx.misc.pop(get_extra_handler_misc_field(info, \"str\"))\n", + "\n", + "\n", + "memory_heap = dict() # This object plays part of some memory heap" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "22bf2660", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:56.714236Z", + "iopub.status.busy": "2023-12-27T16:51:56.713876Z", + "iopub.status.idle": "2023-12-27T16:51:56.719943Z", + "shell.execute_reply": "2023-12-27T16:51:56.719274Z" + } + }, + "outputs": [], + "source": [ + "@to_service(\n", + " before_handler=[time_measure_before_handler, ram_measure_before_handler],\n", + " after_handler=[time_measure_after_handler, ram_measure_after_handler],\n", + ")\n", + "def heavy_service(ctx: Context):\n", + " memory_heap[ctx.last_request.text] = [\n", + " random.randint(0, num) for num in range(0, 1000)\n", + " ]\n", + "\n", + "\n", + "@to_service(\n", + " before_handler=[json_converter_before_handler],\n", + " after_handler=[json_converter_after_handler],\n", + ")\n", + "def logging_service(ctx: Context, _, info: ServiceRuntimeInfo):\n", + " str_misc = ctx.misc[f\"{info.name}-str\"]\n", + " assert isinstance(str_misc, str)\n", + " print(f\"Stringified misc: {str_misc}\")\n", + "\n", + "\n", + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " ServiceGroup(\n", + " before_handler=[time_measure_before_handler],\n", + " after_handler=[time_measure_after_handler],\n", + " components=[heavy_service for _ in range(0, 5)],\n", + " ),\n", + " ACTOR,\n", + " logging_service,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1bd216ca", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:56.722632Z", + "iopub.status.busy": "2023-12-27T16:51:56.722171Z", + "iopub.status.idle": "2023-12-27T16:51:56.761109Z", + "shell.execute_reply": "2023-12-27T16:51:56.760403Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Stringified misc: {\n", + " \"service_group_0-time\": \"0:00:00.006634\",\n", + " \"heavy_service_0-time\": \"0:00:00.001877\",\n", + " \"heavy_service_0-ram\": 0,\n", + " \"heavy_service_1-time\": \"0:00:00.000948\",\n", + " \"heavy_service_1-ram\": 0,\n", + " \"heavy_service_2-time\": \"0:00:00.000886\",\n", + " \"heavy_service_2-ram\": 0,\n", + " \"heavy_service_3-time\": \"0:00:00.000925\",\n", + " \"heavy_service_3-ram\": 0,\n", + " \"heavy_service_4-time\": \"0:00:00.001030\",\n", + " \"heavy_service_4-ram\": 0\n", + "}\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "Stringified misc: {\n", + " \"service_group_0-time\": \"0:00:00.005602\",\n", + " \"heavy_service_0-time\": \"0:00:00.001043\",\n", + " \"heavy_service_0-ram\": 0,\n", + " \"heavy_service_1-time\": \"0:00:00.000935\",\n", + " \"heavy_service_1-ram\": 0,\n", + " \"heavy_service_2-time\": \"0:00:00.000907\",\n", + " \"heavy_service_2-ram\": 0,\n", + " \"heavy_service_3-time\": \"0:00:00.000893\",\n", + " \"heavy_service_3-ram\": 0,\n", + " \"heavy_service_4-time\": \"0:00:00.000936\",\n", + " \"heavy_service_4-ram\": 0\n", + "}\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "Stringified misc: {\n", + " \"service_group_0-time\": \"0:00:00.005418\",\n", + " \"heavy_service_0-time\": \"0:00:00.000920\",\n", + " \"heavy_service_0-ram\": 0,\n", + " \"heavy_service_1-time\": \"0:00:00.000896\",\n", + " \"heavy_service_1-ram\": 0,\n", + " \"heavy_service_2-time\": \"0:00:00.000887\",\n", + " \"heavy_service_2-ram\": 0,\n", + " \"heavy_service_3-time\": \"0:00:00.000920\",\n", + " \"heavy_service_3-ram\": 0,\n", + " \"heavy_service_4-time\": \"0:00:00.000912\",\n", + " \"heavy_service_4-ram\": 0\n", + "}\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "Stringified misc: {\n", + " \"service_group_0-time\": \"0:00:00.005465\",\n", + " \"heavy_service_0-time\": \"0:00:00.000905\",\n", + " \"heavy_service_0-ram\": 0,\n", + " \"heavy_service_1-time\": \"0:00:00.000936\",\n", + " \"heavy_service_1-ram\": 0,\n", + " \"heavy_service_2-time\": \"0:00:00.000921\",\n", + " \"heavy_service_2-ram\": 0,\n", + " \"heavy_service_3-time\": \"0:00:00.000924\",\n", + " \"heavy_service_3-ram\": 0,\n", + " \"heavy_service_4-time\": \"0:00:00.000890\",\n", + " \"heavy_service_4-ram\": 0\n", + "}\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "Stringified misc: {\n", + " \"service_group_0-time\": \"0:00:00.005352\",\n", + " \"heavy_service_0-time\": \"0:00:00.000902\",\n", + " \"heavy_service_0-ram\": 0,\n", + " \"heavy_service_1-time\": \"0:00:00.000879\",\n", + " \"heavy_service_1-ram\": 0,\n", + " \"heavy_service_2-time\": \"0:00:00.000926\",\n", + " \"heavy_service_2-ram\": 0,\n", + " \"heavy_service_3-time\": \"0:00:00.000912\",\n", + " \"heavy_service_3-ram\": 0,\n", + " \"heavy_service_4-time\": \"0:00:00.000886\",\n", + " \"heavy_service_4-ram\": 0\n", + "}\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline(**pipeline_dict)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.ipynb new file mode 100644 index 0000000000..2b0edac5a1 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.ipynb @@ -0,0 +1,287 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2c7f897", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 7. Extra Handlers and Extensions\n", + "\n", + "The following tutorial shows how pipeline can be extended\n", + "by global extra handlers and custom functions.\n", + "\n", + "Here, [add_global_handler](../apiref/dff.pipeline.pipeline.pipeline.rst#dff.pipeline.pipeline.pipeline.Pipeline.add_global_handler)\n", + "function is shown, that can be used to add extra handlers before\n", + "and/or after all pipeline services." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "70d6ef06", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:51:58.409083Z", + "iopub.status.busy": "2023-12-27T16:51:58.408535Z", + "iopub.status.idle": "2023-12-27T16:52:00.716401Z", + "shell.execute_reply": "2023-12-27T16:52:00.715432Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cfec7de", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:00.719346Z", + "iopub.status.busy": "2023-12-27T16:52:00.718905Z", + "iopub.status.idle": "2023-12-27T16:52:01.508830Z", + "shell.execute_reply": "2023-12-27T16:52:01.508001Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "import json\n", + "import logging\n", + "import random\n", + "from datetime import datetime\n", + "\n", + "from dff.pipeline import (\n", + " Pipeline,\n", + " ComponentExecutionState,\n", + " GlobalExtraHandlerType,\n", + " ExtraHandlerRuntimeInfo,\n", + " ServiceRuntimeInfo,\n", + " ACTOR,\n", + ")\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT\n", + "\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "490a3e49", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Pipeline functionality can be extended by global extra handlers.\n", + "Global extra handlers are special extra handlers\n", + " that are called on some stages of pipeline execution.\n", + "There are 4 types of global extra handlers:\n", + "\n", + " * `BEFORE_ALL` is called before pipeline execution.\n", + " * `BEFORE` is called before each service and service group execution.\n", + " * `AFTER` is called after each service and service group execution.\n", + " * `AFTER_ALL` is called after pipeline execution.\n", + "\n", + "Global extra handlers have the same signature as regular extra handlers.\n", + "Actually `BEFORE_ALL` and `AFTER_ALL`\n", + " are attached to root service group named 'pipeline',\n", + " so they return its runtime info\n", + "\n", + "All extra handlers warnings (see tutorial 7)\n", + "are applicable to global extra handlers.\n", + "Pipeline `add_global_extra_handler` function is used to register\n", + " global extra handlers. It accepts following arguments:\n", + "\n", + "* `global_extra_handler_type` (required) - A `GlobalExtraHandlerType` instance,\n", + " indicates extra handler type to add.\n", + "* `extra_handler` (required) - The extra handler function itself.\n", + "* `whitelist` - An optional list of paths, if it's not `None`\n", + " the extra handlers will be applied to\n", + " specified pipeline components only.\n", + "* `blacklist` - An optional list of paths, if it's not `None`\n", + " the extra handlers will be applied to\n", + " all pipeline components except specified.\n", + "\n", + "Here basic functionality of `df-node-stats` library is emulated.\n", + "Information about pipeline component execution time and\n", + " result is collected and printed to info log after pipeline execution.\n", + "Pipeline consists of actor and 25 `long_service`s\n", + "that run random amount of time between 0 and 0.05 seconds." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c7e62598", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:01.512867Z", + "iopub.status.busy": "2023-12-27T16:52:01.512101Z", + "iopub.status.idle": "2023-12-27T16:52:01.522983Z", + "shell.execute_reply": "2023-12-27T16:52:01.522157Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "start_times = dict() # Place to temporarily store service start times\n", + "pipeline_info = dict() # Pipeline information storage\n", + "\n", + "\n", + "def before_all(_, __, info: ExtraHandlerRuntimeInfo):\n", + " global start_times, pipeline_info\n", + " now = datetime.now()\n", + " pipeline_info = {\"start_time\": now}\n", + " start_times = {info.component.path: now}\n", + "\n", + "\n", + "def before(_, __, info: ExtraHandlerRuntimeInfo):\n", + " start_times.update({info.component.path: datetime.now()})\n", + "\n", + "\n", + "def after(_, __, info: ExtraHandlerRuntimeInfo):\n", + " start_time = start_times[info.component.path]\n", + " pipeline_info.update(\n", + " {\n", + " f\"{info.component.path}_duration\": datetime.now() - start_time,\n", + " f\"{info.component.path}_success\": info.component.execution_state.get(\n", + " info.component.path, ComponentExecutionState.NOT_RUN\n", + " ),\n", + " }\n", + " )\n", + "\n", + "\n", + "def after_all(_, __, info: ExtraHandlerRuntimeInfo):\n", + " pipeline_info.update(\n", + " {\"total_time\": datetime.now() - start_times[info.component.path]}\n", + " )\n", + " logger.info(\n", + " f\"Pipeline stats: {json.dumps(pipeline_info, indent=4, default=str)}\"\n", + " )\n", + "\n", + "\n", + "async def long_service(_, __, info: ServiceRuntimeInfo):\n", + " timeout = random.randint(0, 5) / 100\n", + " logger.info(f\"Service {info.name} is going to sleep for {timeout} seconds.\")\n", + " await asyncio.sleep(timeout)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "58821d8e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:01.527288Z", + "iopub.status.busy": "2023-12-27T16:52:01.526697Z", + "iopub.status.idle": "2023-12-27T16:52:01.532437Z", + "shell.execute_reply": "2023-12-27T16:52:01.531767Z" + } + }, + "outputs": [], + "source": [ + "pipeline_dict = {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " [long_service for _ in range(0, 25)],\n", + " ACTOR,\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "30f2d749", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:01.535295Z", + "iopub.status.busy": "2023-12-27T16:52:01.534847Z", + "iopub.status.idle": "2023-12-27T16:52:01.838842Z", + "shell.execute_reply": "2023-12-27T16:52:01.838155Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline(**pipeline_dict)\n", + "\n", + "pipeline.add_global_handler(GlobalExtraHandlerType.BEFORE_ALL, before_all)\n", + "pipeline.add_global_handler(GlobalExtraHandlerType.BEFORE, before)\n", + "pipeline.add_global_handler(GlobalExtraHandlerType.AFTER, after)\n", + "pipeline.add_global_handler(GlobalExtraHandlerType.AFTER_ALL, after_all)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.1_basics.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.1_basics.ipynb new file mode 100644 index 0000000000..842ac40c0d --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.1_basics.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a67a7f1c", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 1. Basics\n", + "\n", + "This notebook shows basic tutorial of creating a simple dialog bot (agent).\n", + "\n", + "Here, basic usege of [Pipeline](../apiref/dff.pipeline.pipeline.pipeline.rst#dff.pipeline.pipeline.pipeline.Pipeline)\n", + "primitive is shown: its' creation with\n", + "[from_script](../apiref/dff.pipeline.pipeline.pipeline.rst#dff.pipeline.pipeline.pipeline.Pipeline.from_script)\n", + "and execution.\n", + "\n", + "Additionally, function [check_happy_path](../apiref/dff.utils.testing.common.rst#dff.utils.testing.common.check_happy_path)\n", + "that can be used for Pipeline testing is presented.\n", + "\n", + "Let's do all the necessary imports from DFF:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "aedb0aa1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:03.574390Z", + "iopub.status.busy": "2023-12-27T16:52:03.573913Z", + "iopub.status.idle": "2023-12-27T16:52:05.887926Z", + "shell.execute_reply": "2023-12-27T16:52:05.887038Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c8b43a72", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:05.891107Z", + "iopub.status.busy": "2023-12-27T16:52:05.890667Z", + "iopub.status.idle": "2023-12-27T16:52:06.597943Z", + "shell.execute_reply": "2023-12-27T16:52:06.597242Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from dff.script import TRANSITIONS, RESPONSE, Message\n", + "from dff.pipeline import Pipeline\n", + "import dff.script.conditions as cnd\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "bb66202c", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "First of all, to create a dialog agent, we need to create a dialog script.\n", + "Below script means a dialog script.\n", + "A script is a dictionary, where the keys are the names of the flows.\n", + "A script can contain multiple scripts, which is needed in order to divide\n", + "a dialog into sub-dialogs and process them separately.\n", + "For example, the separation can be tied to the topic of the dialog.\n", + "In this tutorial there is one flow called `greeting_flow`.\n", + "\n", + "Flow describes a sub-dialog using linked nodes.\n", + "Each node has the keywords `RESPONSE` and `TRANSITIONS`.\n", + "\n", + "* `RESPONSE` contains the response\n", + " that the agent will return from the current node.\n", + "* `TRANSITIONS` describes transitions from the\n", + " current node to another nodes. This is a dictionary,\n", + " where keys are names of the nodes and\n", + " values are conditions of transition to them." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bdb202e6", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:06.601386Z", + "iopub.status.busy": "2023-12-27T16:52:06.600751Z", + "iopub.status.idle": "2023-12-27T16:52:06.609952Z", + "shell.execute_reply": "2023-12-27T16:52:06.609342Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"greeting_flow\": {\n", + " \"start_node\": { # This is the initial node,\n", + " # it doesn't contain a `RESPONSE`.\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " # If \"Hi\" == request of the user then we make the transition.\n", + " },\n", + " \"node1\": {\n", + " RESPONSE: Message(\n", + " text=\"Hi, how are you?\"\n", + " ), # When the agent enters node1,\n", + " # return \"Hi, how are you?\".\n", + " TRANSITIONS: {\n", + " \"node2\": cnd.exact_match(Message(text=\"I'm fine, how are you?\"))\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " \"node3\": cnd.exact_match(\n", + " Message(text=\"Let's talk about music.\")\n", + " )\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about music now.\"),\n", + " TRANSITIONS: {\n", + " \"node4\": cnd.exact_match(Message(text=\"Ok, goodbye.\"))\n", + " },\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"Bye\"),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " \"fallback_node\": {\n", + " # We get to this node if the conditions\n", + " # for switching to other nodes are not performed.\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " }\n", + "}\n", + "\n", + "\n", + "happy_path = (\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(text=\"Hi, how are you?\"),\n", + " ), # start_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"Bye\")), # node3 -> node4\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, how are you?\")), # node4 -> node1\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")), # node1 -> fallback_node\n", + " (\n", + " Message(text=\"stop\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # fallback_node -> fallback_node\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(text=\"Hi, how are you?\"),\n", + " ), # fallback_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"Bye\")), # node3 -> node4\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5f86bf14", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "A `Pipeline` is an object that processes user\n", + "inputs and returns responses.\n", + "To create the pipeline you need to pass the script (`toy_script`),\n", + "initial node (`start_label`) and\n", + "the node to which the default transition will take place\n", + "if none of the current conditions are met (`fallback_label`).\n", + "By default, if `fallback_label` is not set,\n", + "then its value becomes equal to `start_label`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f834d4a4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:06.612747Z", + "iopub.status.busy": "2023-12-27T16:52:06.612261Z", + "iopub.status.idle": "2023-12-27T16:52:06.626597Z", + "shell.execute_reply": "2023-12-27T16:52:06.625929Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='Bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='Bye'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(\n", + " pipeline,\n", + " happy_path,\n", + " ) # This is a function for automatic tutorial\n", + " # running (testing tutorial) with `happy_path`.\n", + "\n", + " # Run tutorial in interactive mode if not in IPython env\n", + " # and if `DISABLE_INTERACTIVE_MODE` is not set.\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)\n", + " # This runs tutorial in interactive mode." + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.2_conditions.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.2_conditions.ipynb new file mode 100644 index 0000000000..247fee1ce2 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.2_conditions.ipynb @@ -0,0 +1,379 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50cd5be0", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 2. Conditions\n", + "\n", + "This tutorial shows different options for\n", + "setting transition conditions from one node to another.\n", + "\n", + "Here, [conditions](../apiref/dff.script.conditions.std_conditions.rst)\n", + "for script transitions are shown.\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d157214f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:08.588162Z", + "iopub.status.busy": "2023-12-27T16:52:08.587948Z", + "iopub.status.idle": "2023-12-27T16:52:10.945500Z", + "shell.execute_reply": "2023-12-27T16:52:10.944711Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f145a266", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:10.948975Z", + "iopub.status.busy": "2023-12-27T16:52:10.948358Z", + "iopub.status.idle": "2023-12-27T16:52:11.685527Z", + "shell.execute_reply": "2023-12-27T16:52:11.684890Z" + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "from dff.script import Context, TRANSITIONS, RESPONSE, Message\n", + "import dff.script.conditions as cnd\n", + "from dff.pipeline import Pipeline\n", + "\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f31703c5", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "The transition condition is set by the function.\n", + "If this function returns the value `True`,\n", + "then the actor performs the corresponding transition.\n", + "Actor is responsible for processing user input and determining\n", + "the appropriate response based on the current state of the conversation and the script.\n", + "See tutorial 1 of pipeline (pipeline/1_basics) to learn more about Actor.\n", + "Condition functions have signature\n", + "\n", + " def func(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool\n", + "\n", + "Out of the box `dff.script.conditions` offers the\n", + " following options for setting conditions:\n", + "\n", + "* `exact_match` returns `True` if the user's request completely\n", + " matches the value passed to the function.\n", + "* `regexp` returns `True` if the pattern matches the user's request,\n", + " while the user's request must be a string.\n", + " `regexp` has same signature as `re.compile` function.\n", + "* `aggregate` returns `bool` value as\n", + " a result after aggregate by `aggregate_func`\n", + " for input sequence of conditions.\n", + " `aggregate_func == any` by default. `aggregate` has alias `agg`.\n", + "* `any` returns `True` if one element of input sequence of conditions is `True`.\n", + " `any(input_sequence)` is equivalent to\n", + " `aggregate(input sequence, aggregate_func=any)`.\n", + "* `all` returns `True` if all elements of input\n", + " sequence of conditions are `True`.\n", + " `all(input_sequence)` is equivalent to\n", + " `aggregate(input sequence, aggregate_func=all)`.\n", + "* `negation` returns negation of passed function. `negation` has alias `neg`.\n", + "* `has_last_labels` covered in the following examples.\n", + "* `true` returns `True`.\n", + "* `false` returns `False`.\n", + "\n", + "For example function\n", + "```\n", + "def always_true_condition(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool:\n", + " return True\n", + "```\n", + "always returns `True` and `always_true_condition` function\n", + "is the same as `dff.script.conditions.std_conditions.true()`.\n", + "\n", + "The functions to be used in the `toy_script` are declared here." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d9114b9a", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:11.688810Z", + "iopub.status.busy": "2023-12-27T16:52:11.688473Z", + "iopub.status.idle": "2023-12-27T16:52:11.694364Z", + "shell.execute_reply": "2023-12-27T16:52:11.693744Z" + } + }, + "outputs": [], + "source": [ + "def hi_lower_case_condition(ctx: Context, _: Pipeline, *args, **kwargs) -> bool:\n", + " request = ctx.last_request\n", + " # Returns True if `hi` in both uppercase and lowercase\n", + " # letters is contained in the user request.\n", + " if request is None or request.text is None:\n", + " return False\n", + " return \"hi\" in request.text.lower()\n", + "\n", + "\n", + "def complex_user_answer_condition(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + ") -> bool:\n", + " request = ctx.last_request\n", + " # The user request can be anything.\n", + " if request is None or request.misc is None:\n", + " return False\n", + " return {\"some_key\": \"some_value\"} == request.misc\n", + "\n", + "\n", + "def predetermined_condition(condition: bool):\n", + " # Wrapper for internal condition function.\n", + " def internal_condition_function(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + " ) -> bool:\n", + " # It always returns `condition`.\n", + " return condition\n", + "\n", + " return internal_condition_function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5a6b1b9", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:11.697186Z", + "iopub.status.busy": "2023-12-27T16:52:11.696792Z", + "iopub.status.idle": "2023-12-27T16:52:11.707835Z", + "shell.execute_reply": "2023-12-27T16:52:11.707160Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"greeting_flow\": {\n", + " \"start_node\": { # This is the initial node,\n", + " # it doesn't contain a `RESPONSE`.\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " # If \"Hi\" == request of user then we make the transition\n", + " },\n", + " \"node1\": {\n", + " RESPONSE: Message(text=\"Hi, how are you?\"),\n", + " TRANSITIONS: {\"node2\": cnd.regexp(r\".*how are you\", re.IGNORECASE)},\n", + " # pattern matching (precompiled)\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " \"node3\": cnd.all(\n", + " [cnd.regexp(r\"talk\"), cnd.regexp(r\"about.*music\")]\n", + " )\n", + " },\n", + " # Mix sequence of conditions by `cnd.all`.\n", + " # `all` is alias `aggregate` with\n", + " # `aggregate_func` == `all`.\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about music now.\"),\n", + " TRANSITIONS: {\"node4\": cnd.regexp(re.compile(r\"Ok, goodbye.\"))},\n", + " # pattern matching by precompiled pattern\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"bye\"),\n", + " TRANSITIONS: {\n", + " \"node1\": cnd.any(\n", + " [\n", + " hi_lower_case_condition,\n", + " cnd.exact_match(Message(text=\"hello\")),\n", + " ]\n", + " )\n", + " },\n", + " # Mix sequence of conditions by `cnd.any`.\n", + " # `any` is alias `aggregate` with\n", + " # `aggregate_func` == `any`.\n", + " },\n", + " \"fallback_node\": { # We get to this node\n", + " # if an error occurred while the agent was running.\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {\n", + " \"node1\": complex_user_answer_condition,\n", + " # The user request can be more than just a string.\n", + " # First we will check returned value of\n", + " # `complex_user_answer_condition`.\n", + " # If the value is `True` then we will go to `node1`.\n", + " # If the value is `False` then we will check a result of\n", + " # `predetermined_condition(True)` for `fallback_node`.\n", + " \"fallback_node\": predetermined_condition(\n", + " True\n", + " ), # or you can use `cnd.true()`\n", + " # Last condition function will return\n", + " # `true` and will repeat `fallback_node`\n", + " # if `complex_user_answer_condition` return `false`.\n", + " },\n", + " },\n", + " }\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(text=\"Hi, how are you?\"),\n", + " ), # start_node -> node1\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")), # node3 -> node4\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, how are you?\")), # node4 -> node1\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")), # node1 -> fallback_node\n", + " (\n", + " Message(text=\"one\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # fallback_node -> fallback_node\n", + " (\n", + " Message(text=\"help\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # fallback_node -> fallback_node\n", + " (\n", + " Message(text=\"nope\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # fallback_node -> fallback_node\n", + " (\n", + " Message(misc={\"some_key\": \"some_value\"}),\n", + " Message(text=\"Hi, how are you?\"),\n", + " ), # fallback_node -> node1\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")), # node3 -> node4\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8e227535", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:11.710531Z", + "iopub.status.busy": "2023-12-27T16:52:11.710080Z", + "iopub.status.idle": "2023-12-27T16:52:11.726397Z", + "shell.execute_reply": "2023-12-27T16:52:11.725706Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='one'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='help'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='nope'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> misc='{'some_key': 'some_value'}'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.3_responses.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.3_responses.ipynb new file mode 100644 index 0000000000..d0477ca40d --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.3_responses.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cfe8b42c", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 3. Responses\n", + "\n", + "This tutorial shows different options for setting responses.\n", + "\n", + "Here, [responses](../apiref/dff.script.responses.std_responses.rst)\n", + "that allow giving custom answers to users are shown.\n", + "\n", + "Let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f3793097", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:13.925985Z", + "iopub.status.busy": "2023-12-27T16:52:13.925765Z", + "iopub.status.idle": "2023-12-27T16:52:16.287210Z", + "shell.execute_reply": "2023-12-27T16:52:16.286404Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "23df1a1d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:16.290301Z", + "iopub.status.busy": "2023-12-27T16:52:16.289861Z", + "iopub.status.idle": "2023-12-27T16:52:17.010644Z", + "shell.execute_reply": "2023-12-27T16:52:17.010012Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import re\n", + "import random\n", + "\n", + "from dff.script import TRANSITIONS, RESPONSE, Context, Message\n", + "import dff.script.responses as rsp\n", + "import dff.script.conditions as cnd\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f6b85042", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "The response can be set by Callable or *Message:\n", + "\n", + "* Callable objects. If the object is callable it must have a special signature:\n", + "\n", + " func(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Any\n", + "\n", + "* *Message objects. If the object is *Message\n", + " it will be returned by the agent as a response.\n", + "\n", + "\n", + "The functions to be used in the `toy_script` are declared here." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "03bd5a8e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:17.013794Z", + "iopub.status.busy": "2023-12-27T16:52:17.013485Z", + "iopub.status.idle": "2023-12-27T16:52:17.020070Z", + "shell.execute_reply": "2023-12-27T16:52:17.019415Z" + } + }, + "outputs": [], + "source": [ + "def cannot_talk_about_topic_response(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + ") -> Message:\n", + " request = ctx.last_request\n", + " if request is None or request.text is None:\n", + " topic = None\n", + " else:\n", + " topic_pattern = re.compile(r\"(.*talk about )(.*)\\.\")\n", + " topic = topic_pattern.findall(request.text)\n", + " topic = topic and topic[0] and topic[0][-1]\n", + " if topic:\n", + " return Message(text=f\"Sorry, I can not talk about {topic} now.\")\n", + " else:\n", + " return Message(text=\"Sorry, I can not talk about that now.\")\n", + "\n", + "\n", + "def upper_case_response(response: Message):\n", + " # wrapper for internal response function\n", + " def func(_: Context, __: Pipeline, *args, **kwargs) -> Message:\n", + " if response.text is not None:\n", + " response.text = response.text.upper()\n", + " return response\n", + "\n", + " return func\n", + "\n", + "\n", + "def fallback_trace_response(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + ") -> Message:\n", + " return Message(\n", + " misc={\n", + " \"previous_node\": list(ctx.labels.values())[-2],\n", + " \"last_request\": ctx.last_request,\n", + " }\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7b23dac4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:17.022686Z", + "iopub.status.busy": "2023-12-27T16:52:17.022309Z", + "iopub.status.idle": "2023-12-27T16:52:17.033330Z", + "shell.execute_reply": "2023-12-27T16:52:17.032716Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"greeting_flow\": {\n", + " \"start_node\": { # This is an initial node,\n", + " # it doesn't need a `RESPONSE`.\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " # If \"Hi\" == request of user then we make the transition\n", + " },\n", + " \"node1\": {\n", + " RESPONSE: rsp.choice(\n", + " [\n", + " Message(text=\"Hi, what is up?\"),\n", + " Message(text=\"Hello, how are you?\"),\n", + " ]\n", + " ),\n", + " # Random choice from candidate list.\n", + " TRANSITIONS: {\n", + " \"node2\": cnd.exact_match(Message(text=\"I'm fine, how are you?\"))\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " \"node3\": cnd.exact_match(\n", + " Message(text=\"Let's talk about music.\")\n", + " )\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: cannot_talk_about_topic_response,\n", + " TRANSITIONS: {\n", + " \"node4\": cnd.exact_match(Message(text=\"Ok, goodbye.\"))\n", + " },\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: upper_case_response(Message(text=\"bye\")),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " \"fallback_node\": { # We get to this node\n", + " # if an error occurred while the agent was running.\n", + " RESPONSE: fallback_trace_response,\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " }\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(text=\"Hello, how are you?\"),\n", + " ), # start_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"BYE\")), # node3 -> node4\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, what is up?\")), # node4 -> node1\n", + " (\n", + " Message(text=\"stop\"),\n", + " Message(\n", + " misc={\n", + " \"previous_node\": (\"greeting_flow\", \"node1\"),\n", + " \"last_request\": Message(text=\"stop\"),\n", + " }\n", + " ),\n", + " ),\n", + " # node1 -> fallback_node\n", + " (\n", + " Message(text=\"one\"),\n", + " Message(\n", + " misc={\n", + " \"previous_node\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"last_request\": Message(text=\"one\"),\n", + " }\n", + " ),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"help\"),\n", + " Message(\n", + " misc={\n", + " \"previous_node\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"last_request\": Message(text=\"help\"),\n", + " }\n", + " ),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"nope\"),\n", + " Message(\n", + " misc={\n", + " \"previous_node\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"last_request\": Message(text=\"nope\"),\n", + " }\n", + " ),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(text=\"Hello, how are you?\"),\n", + " ), # fallback_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"BYE\")), # node3 -> node4\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "87d94e9f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:17.036289Z", + "iopub.status.busy": "2023-12-27T16:52:17.035799Z", + "iopub.status.idle": "2023-12-27T16:52:17.051005Z", + "shell.execute_reply": "2023-12-27T16:52:17.050420Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hello, how are you?'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='BYE'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, what is up?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< misc='{'previous_node': ('greeting_flow', 'node1'), 'last_request': {'text': 'stop'}}'\n", + "(user) >>> text='one'\n", + " (bot) <<< misc='{'previous_node': ('greeting_flow', 'fallback_node'), 'last_request': {'text': 'one'}}'\n", + "(user) >>> text='help'\n", + " (bot) <<< misc='{'previous_node': ('greeting_flow', 'fallback_node'), 'last_request': {'text': 'help'}}'\n", + "(user) >>> text='nope'\n", + " (bot) <<< misc='{'previous_node': ('greeting_flow', 'fallback_node'), 'last_request': {'text': 'nope'}}'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hello, how are you?'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='BYE'\n" + ] + } + ], + "source": [ + "random.seed(31415) # predestination of choice\n", + "\n", + "\n", + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.4_transitions.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.4_transitions.ipynb new file mode 100644 index 0000000000..8c6a7f082c --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.4_transitions.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "678b37b1", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "# Core: 4. Transitions\n", + "\n", + "This tutorial shows settings for transitions between flows and nodes.\n", + "\n", + "Here, [conditions](../apiref/dff.script.conditions.std_conditions.rst)\n", + "for transition between many different script steps are shown.\n", + "\n", + "Some of the destination steps can be set using\n", + "[labels](../apiref/dff.script.labels.std_labels.rst).\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1d6b4cf0", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:18.908749Z", + "iopub.status.busy": "2023-12-27T16:52:18.908465Z", + "iopub.status.idle": "2023-12-27T16:52:21.307216Z", + "shell.execute_reply": "2023-12-27T16:52:21.306388Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a16b8ba6", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:21.310291Z", + "iopub.status.busy": "2023-12-27T16:52:21.309997Z", + "iopub.status.idle": "2023-12-27T16:52:22.014409Z", + "shell.execute_reply": "2023-12-27T16:52:22.013674Z" + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "from dff.script import TRANSITIONS, RESPONSE, Context, NodeLabel3Type, Message\n", + "import dff.script.conditions as cnd\n", + "import dff.script.labels as lbl\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9327de62", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Let's define the functions with a special type of return value:\n", + "\n", + " NodeLabel3Type == tuple[str, str, float]\n", + "\n", + "which means that transition returns a `tuple`\n", + "with flow name, node name and priority." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "986877f1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:22.017822Z", + "iopub.status.busy": "2023-12-27T16:52:22.017357Z", + "iopub.status.idle": "2023-12-27T16:52:22.021533Z", + "shell.execute_reply": "2023-12-27T16:52:22.020901Z" + } + }, + "outputs": [], + "source": [ + "def greeting_flow_n2_transition(\n", + " _: Context, __: Pipeline, *args, **kwargs\n", + ") -> NodeLabel3Type:\n", + " return (\"greeting_flow\", \"node2\", 1.0)\n", + "\n", + "\n", + "def high_priority_node_transition(flow_label, label):\n", + " def transition(_: Context, __: Pipeline, *args, **kwargs) -> NodeLabel3Type:\n", + " return (flow_label, label, 2.0)\n", + "\n", + " return transition" + ] + }, + { + "cell_type": "markdown", + "id": "d3aaab5c", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Priority is needed to select a condition\n", + "in the situation where more than one condition is `True`.\n", + "All conditions in `TRANSITIONS` are being checked.\n", + "Of the set of `True` conditions,\n", + "the one that has the highest priority will be executed.\n", + "Of the set of `True` conditions with largest\n", + "priority the first met condition will be executed.\n", + "\n", + "Out of the box `dff.script.core.labels`\n", + "offers the following methods:\n", + "\n", + "* `lbl.repeat()` returns transition handler\n", + " which returns `NodeLabelType` to the last node,\n", + "\n", + "* `lbl.previous()` returns transition handler\n", + " which returns `NodeLabelType` to the previous node,\n", + "\n", + "* `lbl.to_start()` returns transition handler\n", + " which returns `NodeLabelType` to the start node,\n", + "\n", + "* `lbl.to_fallback()` returns transition\n", + " handler which returns `NodeLabelType` to the fallback node,\n", + "\n", + "* `lbl.forward()` returns transition handler\n", + " which returns `NodeLabelType` to the forward node,\n", + "\n", + "* `lbl.backward()` returns transition handler\n", + " which returns `NodeLabelType` to the backward node.\n", + "\n", + "There are three flows here: `global_flow`, `greeting_flow`, `music_flow`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f5615365", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:22.024036Z", + "iopub.status.busy": "2023-12-27T16:52:22.023844Z", + "iopub.status.idle": "2023-12-27T16:52:22.042030Z", + "shell.execute_reply": "2023-12-27T16:52:22.041373Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"global_flow\": {\n", + " \"start_node\": { # This is an initial node,\n", + " # it doesn't need a `RESPONSE`.\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {\n", + " (\"music_flow\", \"node1\"): cnd.regexp(\n", + " r\"talk about music\"\n", + " ), # first check\n", + " (\"greeting_flow\", \"node1\"): cnd.regexp(\n", + " r\"hi|hello\", re.IGNORECASE\n", + " ), # second check\n", + " \"fallback_node\": cnd.true(), # third check\n", + " # \"fallback_node\" is equivalent to\n", + " # (\"global_flow\", \"fallback_node\").\n", + " },\n", + " },\n", + " \"fallback_node\": { # We get to this node if\n", + " # an error occurred while the agent was running.\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {\n", + " (\"music_flow\", \"node1\"): cnd.regexp(\n", + " r\"talk about music\"\n", + " ), # first check\n", + " (\"greeting_flow\", \"node1\"): cnd.regexp(\n", + " r\"hi|hello\", re.IGNORECASE\n", + " ), # second check\n", + " lbl.previous(): cnd.regexp(\n", + " r\"previous\", re.IGNORECASE\n", + " ), # third check\n", + " # lbl.previous() is equivalent\n", + " # to (\"previous_flow\", \"previous_node\")\n", + " lbl.repeat(): cnd.true(), # fourth check\n", + " # lbl.repeat() is equivalent to (\"global_flow\", \"fallback_node\")\n", + " },\n", + " },\n", + " },\n", + " \"greeting_flow\": {\n", + " \"node1\": {\n", + " RESPONSE: Message(text=\"Hi, how are you?\"),\n", + " # When the agent goes to node1, we return \"Hi, how are you?\"\n", + " TRANSITIONS: {\n", + " (\n", + " \"global_flow\",\n", + " \"fallback_node\",\n", + " 0.1,\n", + " ): cnd.true(), # second check\n", + " \"node2\": cnd.regexp(r\"how are you\"), # first check\n", + " # \"node2\" is equivalent to (\"greeting_flow\", \"node2\", 1.0)\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " lbl.to_fallback(0.1): cnd.true(), # third check\n", + " # lbl.to_fallback(0.1) is equivalent\n", + " # to (\"global_flow\", \"fallback_node\", 0.1)\n", + " lbl.forward(0.5): cnd.regexp(r\"talk about\"), # second check\n", + " # lbl.forward(0.5) is equivalent\n", + " # to (\"greeting_flow\", \"node3\", 0.5)\n", + " (\"music_flow\", \"node1\"): cnd.regexp(\n", + " r\"talk about music\"\n", + " ), # first check\n", + " # (\"music_flow\", \"node1\") is equivalent\n", + " # to (\"music_flow\", \"node1\", 1.0)\n", + " lbl.previous(): cnd.regexp(\n", + " r\"previous\", re.IGNORECASE\n", + " ), # third check\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about that now.\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.regexp(r\"bye\")},\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"Bye\"),\n", + " TRANSITIONS: {\n", + " \"node1\": cnd.regexp(r\"hi|hello\", re.IGNORECASE), # first check\n", + " lbl.to_fallback(): cnd.true(), # second check\n", + " },\n", + " },\n", + " },\n", + " \"music_flow\": {\n", + " \"node1\": {\n", + " RESPONSE: Message(\n", + " text=\"I love `System of a Down` group, \"\n", + " \"would you like to talk about it?\"\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): cnd.regexp(r\"yes|yep|ok\", re.IGNORECASE),\n", + " lbl.to_fallback(): cnd.true(),\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): cnd.regexp(r\"next\", re.IGNORECASE),\n", + " lbl.repeat(): cnd.regexp(r\"repeat\", re.IGNORECASE),\n", + " lbl.to_fallback(): cnd.true(),\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): cnd.regexp(r\"next\", re.IGNORECASE),\n", + " lbl.backward(): cnd.regexp(r\"back\", re.IGNORECASE),\n", + " lbl.repeat(): cnd.regexp(r\"repeat\", re.IGNORECASE),\n", + " lbl.to_fallback(): cnd.true(),\n", + " },\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"That's all what I know.\"),\n", + " TRANSITIONS: {\n", + " greeting_flow_n2_transition: cnd.regexp(\n", + " r\"next\", re.IGNORECASE\n", + " ), # second check\n", + " high_priority_node_transition(\n", + " \"greeting_flow\", \"node4\"\n", + " ): cnd.regexp(\n", + " r\"next time\", re.IGNORECASE\n", + " ), # first check\n", + " lbl.to_fallback(): cnd.true(), # third check\n", + " },\n", + " },\n", + " },\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (Message(text=\"hi\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"talk about music.\"),\n", + " Message(\n", + " text=\"I love `System of a Down` group, \"\n", + " \"would you like to talk about it?\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"yes\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"back\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"repeat\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " ),\n", + " (Message(text=\"next\"), Message(text=\"That's all what I know.\")),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (Message(text=\"previous\"), Message(text=\"That's all what I know.\")),\n", + " (Message(text=\"next time\"), Message(text=\"Bye\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"previous\"), Message(text=\"Bye\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"nope\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"hi\"), Message(text=\"Hi, how are you?\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"previous\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"let's talk about something.\"),\n", + " Message(text=\"Sorry, I can not talk about that now.\"),\n", + " ),\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"Bye\")),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "791ec15b", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:22.044555Z", + "iopub.status.busy": "2023-12-27T16:52:22.044181Z", + "iopub.status.idle": "2023-12-27T16:52:22.068809Z", + "shell.execute_reply": "2023-12-27T16:52:22.068203Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='talk about music.'\n", + " (bot) <<< text='I love `System of a Down` group, would you like to talk about it?'\n", + "(user) >>> text='yes'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='The band achieved commercial success with the release of five studio albums.'\n", + "(user) >>> text='back'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='repeat'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='The band achieved commercial success with the release of five studio albums.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='That's all what I know.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='That's all what I know.'\n", + "(user) >>> text='next time'\n", + " (bot) <<< text='Bye'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='Bye'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='nope'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='let's talk about something.'\n", + " (bot) <<< text='Sorry, I can not talk about that now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='Bye'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"global_flow\", \"start_node\"),\n", + " fallback_label=(\"global_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.5_global_transitions.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.5_global_transitions.ipynb new file mode 100644 index 0000000000..af80d5bbfd --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.5_global_transitions.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5591176d", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "# Core: 5. Global transitions\n", + "\n", + "This tutorial shows the global setting of transitions.\n", + "\n", + "Here, global [conditions](../apiref/dff.script.conditions.std_conditions.rst)\n", + "for default transition between many different script steps are shown.\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af0c88e9", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:23.809542Z", + "iopub.status.busy": "2023-12-27T16:52:23.809105Z", + "iopub.status.idle": "2023-12-27T16:52:26.275399Z", + "shell.execute_reply": "2023-12-27T16:52:26.274555Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "26cedb91", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:26.278503Z", + "iopub.status.busy": "2023-12-27T16:52:26.278042Z", + "iopub.status.idle": "2023-12-27T16:52:26.991393Z", + "shell.execute_reply": "2023-12-27T16:52:26.990674Z" + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "from dff.script import GLOBAL, TRANSITIONS, RESPONSE, Message\n", + "import dff.script.conditions as cnd\n", + "import dff.script.labels as lbl\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "347debad", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "The keyword `GLOBAL` is used to define a global node.\n", + "There can be only one global node in a script.\n", + "The value that corresponds to this key has the\n", + "`dict` type with the same keywords as regular nodes.\n", + "The global node is defined above the flow level as opposed to regular nodes.\n", + "This node allows to define default global values for all nodes.\n", + "\n", + "There are `GLOBAL` node and three flows:\n", + "`global_flow`, `greeting_flow`, `music_flow`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f2e829c1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:26.994802Z", + "iopub.status.busy": "2023-12-27T16:52:26.994342Z", + "iopub.status.idle": "2023-12-27T16:52:27.011884Z", + "shell.execute_reply": "2023-12-27T16:52:27.011241Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " GLOBAL: {\n", + " TRANSITIONS: {\n", + " (\"greeting_flow\", \"node1\", 1.1): cnd.regexp(\n", + " r\"\\b(hi|hello)\\b\", re.I\n", + " ), # first check\n", + " (\"music_flow\", \"node1\", 1.1): cnd.regexp(\n", + " r\"talk about music\"\n", + " ), # second check\n", + " lbl.to_fallback(0.1): cnd.true(), # fifth check\n", + " lbl.forward(): cnd.all(\n", + " [\n", + " cnd.regexp(r\"next\\b\"),\n", + " cnd.has_last_labels(\n", + " labels=[(\"music_flow\", i) for i in [\"node2\", \"node3\"]]\n", + " ),\n", + " ] # third check\n", + " ),\n", + " lbl.repeat(0.2): cnd.all(\n", + " [\n", + " cnd.regexp(r\"repeat\", re.I),\n", + " cnd.negation(\n", + " cnd.has_last_labels(flow_labels=[\"global_flow\"])\n", + " ),\n", + " ] # fourth check\n", + " ),\n", + " }\n", + " },\n", + " \"global_flow\": {\n", + " \"start_node\": {\n", + " RESPONSE: Message()\n", + " }, # This is an initial node, it doesn't need a `RESPONSE`.\n", + " \"fallback_node\": { # We get to this node\n", + " # if an error occurred while the agent was running.\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {lbl.previous(): cnd.regexp(r\"previous\", re.I)},\n", + " # lbl.previous() is equivalent to\n", + " # (\"previous_flow\", \"previous_node\", 1.0)\n", + " },\n", + " },\n", + " \"greeting_flow\": {\n", + " \"node1\": {\n", + " RESPONSE: Message(text=\"Hi, how are you?\"),\n", + " TRANSITIONS: {\"node2\": cnd.regexp(r\"how are you\")},\n", + " # \"node2\" is equivalent to (\"greeting_flow\", \"node2\", 1.0)\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " lbl.forward(0.5): cnd.regexp(r\"talk about\"),\n", + " # lbl.forward(0.5) is equivalent to\n", + " # (\"greeting_flow\", \"node3\", 0.5)\n", + " lbl.previous(): cnd.regexp(r\"previous\", re.I),\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about that now.\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.regexp(r\"bye\")},\n", + " },\n", + " \"node4\": {RESPONSE: Message(text=\"bye\")},\n", + " # Only the global transitions setting are used in this node.\n", + " },\n", + " \"music_flow\": {\n", + " \"node1\": {\n", + " RESPONSE: Message(\n", + " text=\"I love `System of a Down` group, \"\n", + " \"would you like to talk about it?\"\n", + " ),\n", + " TRANSITIONS: {lbl.forward(): cnd.regexp(r\"yes|yep|ok\", re.I)},\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " )\n", + " # Only the global transitions setting are used in this node.\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " TRANSITIONS: {lbl.backward(): cnd.regexp(r\"back\", re.I)},\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"That's all what I know.\"),\n", + " TRANSITIONS: {\n", + " (\"greeting_flow\", \"node4\"): cnd.regexp(r\"next time\", re.I),\n", + " (\"greeting_flow\", \"node2\"): cnd.regexp(r\"next\", re.I),\n", + " },\n", + " },\n", + " },\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (Message(text=\"hi\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"talk about music.\"),\n", + " Message(\n", + " text=\"I love `System of a Down` group, \"\n", + " \"would you like to talk about it?\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"yes\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"back\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"repeat\"),\n", + " Message(\n", + " text=\"System of a Down is \"\n", + " \"an Armenian-American heavy metal band formed in 1994.\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(\n", + " text=\"The band achieved commercial success \"\n", + " \"with the release of five studio albums.\"\n", + " ),\n", + " ),\n", + " (Message(text=\"next\"), Message(text=\"That's all what I know.\")),\n", + " (\n", + " Message(text=\"next\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (Message(text=\"previous\"), Message(text=\"That's all what I know.\")),\n", + " (Message(text=\"next time\"), Message(text=\"bye\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"previous\"), Message(text=\"bye\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"nope\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"hi\"), Message(text=\"Hi, how are you?\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"previous\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"let's talk about something.\"),\n", + " Message(text=\"Sorry, I can not talk about that now.\"),\n", + " ),\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e06d3be4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:27.014531Z", + "iopub.status.busy": "2023-12-27T16:52:27.014170Z", + "iopub.status.idle": "2023-12-27T16:52:27.038847Z", + "shell.execute_reply": "2023-12-27T16:52:27.038150Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='talk about music.'\n", + " (bot) <<< text='I love `System of a Down` group, would you like to talk about it?'\n", + "(user) >>> text='yes'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='The band achieved commercial success with the release of five studio albums.'\n", + "(user) >>> text='back'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='repeat'\n", + " (bot) <<< text='System of a Down is an Armenian-American heavy metal band formed in 1994.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='The band achieved commercial success with the release of five studio albums.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='That's all what I know.'\n", + "(user) >>> text='next'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='That's all what I know.'\n", + "(user) >>> text='next time'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='nope'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='previous'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='let's talk about something.'\n", + " (bot) <<< text='Sorry, I can not talk about that now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"global_flow\", \"start_node\"),\n", + " fallback_label=(\"global_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.6_context_serialization.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.6_context_serialization.ipynb new file mode 100644 index 0000000000..361f329c8a --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.6_context_serialization.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c1980dff", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 6. Context serialization\n", + "\n", + "This tutorial shows context serialization.\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "22be9e70", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:28.874674Z", + "iopub.status.busy": "2023-12-27T16:52:28.874490Z", + "iopub.status.idle": "2023-12-27T16:52:31.457138Z", + "shell.execute_reply": "2023-12-27T16:52:31.456340Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "15854fd3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:31.460016Z", + "iopub.status.busy": "2023-12-27T16:52:31.459728Z", + "iopub.status.idle": "2023-12-27T16:52:32.168881Z", + "shell.execute_reply": "2023-12-27T16:52:32.168240Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "from dff.script import TRANSITIONS, RESPONSE, Context, Message\n", + "import dff.script.conditions as cnd\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "17ec3658", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "This function returns the user request number." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "329261a3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:32.172318Z", + "iopub.status.busy": "2023-12-27T16:52:32.171718Z", + "iopub.status.idle": "2023-12-27T16:52:32.175173Z", + "shell.execute_reply": "2023-12-27T16:52:32.174629Z" + } + }, + "outputs": [], + "source": [ + "def response_handler(ctx: Context, _: Pipeline, *args, **kwargs) -> Message:\n", + " return Message(text=f\"answer {len(ctx.requests)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2bf8da2d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:32.177537Z", + "iopub.status.busy": "2023-12-27T16:52:32.177276Z", + "iopub.status.idle": "2023-12-27T16:52:32.181731Z", + "shell.execute_reply": "2023-12-27T16:52:32.181174Z" + } + }, + "outputs": [], + "source": [ + "# a dialog script\n", + "toy_script = {\n", + " \"flow_start\": {\n", + " \"node_start\": {\n", + " RESPONSE: response_handler,\n", + " TRANSITIONS: {(\"flow_start\", \"node_start\"): cnd.true()},\n", + " }\n", + " }\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (Message(text=\"hi\"), Message(text=\"answer 1\")),\n", + " (Message(text=\"how are you?\"), Message(text=\"answer 2\")),\n", + " (Message(text=\"ok\"), Message(text=\"answer 3\")),\n", + " (Message(text=\"good\"), Message(text=\"answer 4\")),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "107a9f4e", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "Draft function that performs serialization." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "edf54b4a", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:32.184321Z", + "iopub.status.busy": "2023-12-27T16:52:32.183921Z", + "iopub.status.idle": "2023-12-27T16:52:32.188367Z", + "shell.execute_reply": "2023-12-27T16:52:32.187774Z" + } + }, + "outputs": [], + "source": [ + "def process_response(ctx: Context):\n", + " ctx_json = ctx.model_dump_json()\n", + " if isinstance(ctx_json, str):\n", + " logging.info(\"context serialized to json str\")\n", + " else:\n", + " raise Exception(f\"ctx={ctx_json} has to be serialized to json string\")\n", + "\n", + " ctx_dict = ctx.model_dump()\n", + " if isinstance(ctx_dict, dict):\n", + " logging.info(\"context serialized to dict\")\n", + " else:\n", + " raise Exception(f\"ctx={ctx_dict} has to be serialized to dict\")\n", + "\n", + " if not isinstance(ctx, Context):\n", + " raise Exception(f\"ctx={ctx} has to have Context type\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cff36838", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:32.190766Z", + "iopub.status.busy": "2023-12-27T16:52:32.190418Z", + "iopub.status.idle": "2023-12-27T16:52:32.199726Z", + "shell.execute_reply": "2023-12-27T16:52:32.199032Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='hi'\n", + " (bot) <<< text='answer 1'\n", + "(user) >>> text='how are you?'\n", + " (bot) <<< text='answer 2'\n", + "(user) >>> text='ok'\n", + " (bot) <<< text='answer 3'\n", + "(user) >>> text='good'\n", + " (bot) <<< text='answer 4'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"flow_start\", \"node_start\"),\n", + " post_services=[process_response],\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.7_pre_response_processing.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.7_pre_response_processing.ipynb new file mode 100644 index 0000000000..e08dbaf87b --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.7_pre_response_processing.ipynb @@ -0,0 +1,709 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a703e238", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 7. Pre-response processing\n", + "\n", + "This tutorial shows pre-response processing feature.\n", + "\n", + "Here, [PRE_RESPONSE_PROCESSING](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords.PRE_RESPONSE_PROCESSING)\n", + "is demonstrated which can be used for additional context processing before response handlers.\n", + "\n", + "There are also some other [Keywords](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords)\n", + "worth attention used in this tutorial.\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "dc2641f7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:33.960546Z", + "iopub.status.busy": "2023-12-27T16:52:33.960054Z", + "iopub.status.idle": "2023-12-27T16:52:36.318036Z", + "shell.execute_reply": "2023-12-27T16:52:36.317060Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "13cf6c1f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:36.322021Z", + "iopub.status.busy": "2023-12-27T16:52:36.321552Z", + "iopub.status.idle": "2023-12-27T16:52:37.136154Z", + "shell.execute_reply": "2023-12-27T16:52:37.135482Z" + } + }, + "outputs": [], + "source": [ + "from dff.script import (\n", + " GLOBAL,\n", + " LOCAL,\n", + " RESPONSE,\n", + " TRANSITIONS,\n", + " PRE_RESPONSE_PROCESSING,\n", + " Context,\n", + " Message,\n", + ")\n", + "import dff.script.labels as lbl\n", + "import dff.script.conditions as cnd\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "925be2b4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:37.139484Z", + "iopub.status.busy": "2023-12-27T16:52:37.138845Z", + "iopub.status.idle": "2023-12-27T16:52:37.143094Z", + "shell.execute_reply": "2023-12-27T16:52:37.142442Z" + } + }, + "outputs": [], + "source": [ + "def add_prefix(prefix):\n", + " def add_prefix_processing(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + " ) -> Context:\n", + " processed_node = ctx.current_node\n", + " processed_node.response = Message(\n", + " text=f\"{prefix}: {processed_node.response.text}\"\n", + " )\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " return ctx\n", + "\n", + " return add_prefix_processing" + ] + }, + { + "cell_type": "markdown", + "id": "d3d4fe32", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "`PRE_RESPONSE_PROCESSING` is a keyword that\n", + "can be used in `GLOBAL`, `LOCAL` or nodes." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2f33d805", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:37.145918Z", + "iopub.status.busy": "2023-12-27T16:52:37.145428Z", + "iopub.status.idle": "2023-12-27T16:52:37.153722Z", + "shell.execute_reply": "2023-12-27T16:52:37.153101Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"root\": {\n", + " \"start\": {\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {(\"flow\", \"step_0\"): cnd.true()},\n", + " },\n", + " \"fallback\": {RESPONSE: Message(text=\"the end\")},\n", + " },\n", + " GLOBAL: {\n", + " PRE_RESPONSE_PROCESSING: {\n", + " \"proc_name_1\": add_prefix(\"l1_global\"),\n", + " \"proc_name_2\": add_prefix(\"l2_global\"),\n", + " }\n", + " },\n", + " \"flow\": {\n", + " LOCAL: {\n", + " PRE_RESPONSE_PROCESSING: {\n", + " \"proc_name_2\": add_prefix(\"l2_local\"),\n", + " \"proc_name_3\": add_prefix(\"l3_local\"),\n", + " }\n", + " },\n", + " \"step_0\": {\n", + " RESPONSE: Message(text=\"first\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_1\": {\n", + " PRE_RESPONSE_PROCESSING: {\"proc_name_1\": add_prefix(\"l1_step_1\")},\n", + " RESPONSE: Message(text=\"second\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_2\": {\n", + " PRE_RESPONSE_PROCESSING: {\"proc_name_2\": add_prefix(\"l2_step_2\")},\n", + " RESPONSE: Message(text=\"third\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_3\": {\n", + " PRE_RESPONSE_PROCESSING: {\"proc_name_3\": add_prefix(\"l3_step_3\")},\n", + " RESPONSE: Message(text=\"fourth\"),\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_4\": {\n", + " PRE_RESPONSE_PROCESSING: {\"proc_name_4\": add_prefix(\"l4_step_4\")},\n", + " RESPONSE: Message(text=\"fifth\"),\n", + " TRANSITIONS: {\"step_0\": cnd.true()},\n", + " },\n", + " },\n", + "}\n", + "\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (Message(), Message(text=\"l3_local: l2_local: l1_global: first\")),\n", + " (Message(), Message(text=\"l3_local: l2_local: l1_step_1: second\")),\n", + " (Message(), Message(text=\"l3_local: l2_step_2: l1_global: third\")),\n", + " (Message(), Message(text=\"l3_step_3: l2_local: l1_global: fourth\")),\n", + " (\n", + " Message(),\n", + " Message(text=\"l4_step_4: l3_local: l2_local: l1_global: fifth\"),\n", + " ),\n", + " (Message(), Message(text=\"l3_local: l2_local: l1_global: first\")),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0711adda", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:37.156338Z", + "iopub.status.busy": "2023-12-27T16:52:37.155965Z", + "iopub.status.idle": "2023-12-27T16:52:37.184051Z", + "shell.execute_reply": "2023-12-27T16:52:37.183390Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16079d30>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160044c0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004550>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16004700>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160044c0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004550>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16079d30>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160048b0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004550>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16079d30>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160044c0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004a60>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16079d30>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160044c0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004550>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_4 and processing_func=.add_prefix_processing at 0x7f6c16004c10>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=.add_prefix_processing at 0x7f6c16079d30>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_2 and processing_func=.add_prefix_processing at 0x7f6c160044c0>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_3 and processing_func=.add_prefix_processing at 0x7f6c16004550>\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_21612/562700550.py\", line 9, in add_prefix_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> \n", + " (bot) <<< text='l3_local: l2_local: l1_global: first'\n", + "(user) >>> \n", + " (bot) <<< text='l3_local: l2_local: l1_step_1: second'\n", + "(user) >>> \n", + " (bot) <<< text='l3_local: l2_step_2: l1_global: third'\n", + "(user) >>> \n", + " (bot) <<< text='l3_step_3: l2_local: l1_global: fourth'\n", + "(user) >>> \n", + " (bot) <<< text='l4_step_4: l3_local: l2_local: l1_global: fifth'\n", + "(user) >>> \n", + " (bot) <<< text='l3_local: l2_local: l1_global: first'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"root\", \"start\"),\n", + " fallback_label=(\"root\", \"fallback\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.8_misc.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.8_misc.ipynb new file mode 100644 index 0000000000..3d89e6a007 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.8_misc.ipynb @@ -0,0 +1,299 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f08c12e6", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 8. Misc\n", + "\n", + "This tutorial shows `MISC` (miscellaneous) keyword usage.\n", + "\n", + "See [MISC](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords.MISC)\n", + "for more information.\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7a95c475", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:39.098646Z", + "iopub.status.busy": "2023-12-27T16:52:39.098436Z", + "iopub.status.idle": "2023-12-27T16:52:41.405256Z", + "shell.execute_reply": "2023-12-27T16:52:41.404465Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "71658d45", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:41.408501Z", + "iopub.status.busy": "2023-12-27T16:52:41.408011Z", + "iopub.status.idle": "2023-12-27T16:52:42.163158Z", + "shell.execute_reply": "2023-12-27T16:52:42.162320Z" + } + }, + "outputs": [], + "source": [ + "from dff.script import (\n", + " GLOBAL,\n", + " LOCAL,\n", + " RESPONSE,\n", + " TRANSITIONS,\n", + " MISC,\n", + " Context,\n", + " Message,\n", + ")\n", + "import dff.script.labels as lbl\n", + "import dff.script.conditions as cnd\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bf4598eb", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:42.167124Z", + "iopub.status.busy": "2023-12-27T16:52:42.166751Z", + "iopub.status.idle": "2023-12-27T16:52:42.171972Z", + "shell.execute_reply": "2023-12-27T16:52:42.171049Z" + } + }, + "outputs": [], + "source": [ + "def custom_response(ctx: Context, _: Pipeline, *args, **kwargs) -> Message:\n", + " if ctx.validation:\n", + " return Message()\n", + " current_node = ctx.current_node\n", + " return Message(\n", + " text=f\"ctx.last_label={ctx.last_label}: \"\n", + " f\"current_node.misc={current_node.misc}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "14da3365", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:42.175009Z", + "iopub.status.busy": "2023-12-27T16:52:42.174552Z", + "iopub.status.idle": "2023-12-27T16:52:42.184359Z", + "shell.execute_reply": "2023-12-27T16:52:42.183660Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"root\": {\n", + " \"start\": {\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {(\"flow\", \"step_0\"): cnd.true()},\n", + " },\n", + " \"fallback\": {RESPONSE: Message(text=\"the end\")},\n", + " },\n", + " GLOBAL: {\n", + " MISC: {\n", + " \"var1\": \"global_data\",\n", + " \"var2\": \"global_data\",\n", + " \"var3\": \"global_data\",\n", + " }\n", + " },\n", + " \"flow\": {\n", + " LOCAL: {\n", + " MISC: {\n", + " \"var2\": \"rewrite_by_local\",\n", + " \"var3\": \"rewrite_by_local\",\n", + " }\n", + " },\n", + " \"step_0\": {\n", + " MISC: {\"var3\": \"info_of_step_0\"},\n", + " RESPONSE: custom_response,\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_1\": {\n", + " MISC: {\"var3\": \"info_of_step_1\"},\n", + " RESPONSE: custom_response,\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_2\": {\n", + " MISC: {\"var3\": \"info_of_step_2\"},\n", + " RESPONSE: custom_response,\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_3\": {\n", + " MISC: {\"var3\": \"info_of_step_3\"},\n", + " RESPONSE: custom_response,\n", + " TRANSITIONS: {lbl.forward(): cnd.true()},\n", + " },\n", + " \"step_4\": {\n", + " MISC: {\"var3\": \"info_of_step_4\"},\n", + " RESPONSE: custom_response,\n", + " TRANSITIONS: {\"step_0\": cnd.true()},\n", + " },\n", + " },\n", + "}\n", + "\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_0'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_0'}\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_1'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_1'}\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_2'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_2'}\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_3'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_3'}\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_4'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_4'}\"\n", + " ),\n", + " ),\n", + " (\n", + " Message(),\n", + " Message(\n", + " text=\"ctx.last_label=('flow', 'step_0'): current_node.misc=\"\n", + " \"{'var1': 'global_data', \"\n", + " \"'var2': 'rewrite_by_local', \"\n", + " \"'var3': 'info_of_step_0'}\"\n", + " ),\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bd520ce4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:42.187608Z", + "iopub.status.busy": "2023-12-27T16:52:42.186990Z", + "iopub.status.idle": "2023-12-27T16:52:42.201461Z", + "shell.execute_reply": "2023-12-27T16:52:42.200714Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_0'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_0'}'\n", + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_1'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_1'}'\n", + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_2'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_2'}'\n", + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_3'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_3'}'\n", + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_4'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_4'}'\n", + "(user) >>> \n", + " (bot) <<< text='ctx.last_label=('flow', 'step_0'): current_node.misc={'var1': 'global_data', 'var2': 'rewrite_by_local', 'var3': 'info_of_step_0'}'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"root\", \"start\"),\n", + " fallback_label=(\"root\", \"fallback\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.core.9_pre_transitions_processing.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.core.9_pre_transitions_processing.ipynb new file mode 100644 index 0000000000..5b41863c06 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.core.9_pre_transitions_processing.ipynb @@ -0,0 +1,351 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "39aceda9", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Core: 9. Pre-transitions processing\n", + "\n", + "This tutorial shows pre-transitions processing feature.\n", + "\n", + "Here, [PRE_TRANSITIONS_PROCESSING](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords.PRE_TRANSITIONS_PROCESSING)\n", + "is demonstrated which can be used for additional context\n", + "processing before transitioning to the next step.\n", + "\n", + "First of all, let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "66b1f9c6", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:43.863365Z", + "iopub.status.busy": "2023-12-27T16:52:43.863168Z", + "iopub.status.idle": "2023-12-27T16:52:46.152417Z", + "shell.execute_reply": "2023-12-27T16:52:46.151390Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5a11968b", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:46.156363Z", + "iopub.status.busy": "2023-12-27T16:52:46.155808Z", + "iopub.status.idle": "2023-12-27T16:52:46.880234Z", + "shell.execute_reply": "2023-12-27T16:52:46.879593Z" + } + }, + "outputs": [], + "source": [ + "from dff.script import (\n", + " GLOBAL,\n", + " RESPONSE,\n", + " TRANSITIONS,\n", + " PRE_RESPONSE_PROCESSING,\n", + " PRE_TRANSITIONS_PROCESSING,\n", + " Context,\n", + " Message,\n", + ")\n", + "import dff.script.labels as lbl\n", + "import dff.script.conditions as cnd\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7435551e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:46.883361Z", + "iopub.status.busy": "2023-12-27T16:52:46.882869Z", + "iopub.status.idle": "2023-12-27T16:52:46.887427Z", + "shell.execute_reply": "2023-12-27T16:52:46.886779Z" + } + }, + "outputs": [], + "source": [ + "def save_previous_node_response_to_ctx_processing(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + ") -> Context:\n", + " processed_node = ctx.current_node\n", + " ctx.misc[\"previous_node_response\"] = processed_node.response\n", + " return ctx\n", + "\n", + "\n", + "def get_previous_node_response_for_response_processing(\n", + " ctx: Context, _: Pipeline, *args, **kwargs\n", + ") -> Context:\n", + " processed_node = ctx.current_node\n", + " processed_node.response = Message(\n", + " text=f\"previous={ctx.misc['previous_node_response'].text}:\"\n", + " f\" current={processed_node.response.text}\"\n", + " )\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " return ctx" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b9deabc2", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:46.889990Z", + "iopub.status.busy": "2023-12-27T16:52:46.889564Z", + "iopub.status.idle": "2023-12-27T16:52:46.895986Z", + "shell.execute_reply": "2023-12-27T16:52:46.895351Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# a dialog script\n", + "toy_script = {\n", + " \"root\": {\n", + " \"start\": {\n", + " RESPONSE: Message(),\n", + " TRANSITIONS: {(\"flow\", \"step_0\"): cnd.true()},\n", + " },\n", + " \"fallback\": {RESPONSE: Message(text=\"the end\")},\n", + " },\n", + " GLOBAL: {\n", + " PRE_RESPONSE_PROCESSING: {\n", + " \"proc_name_1\": get_previous_node_response_for_response_processing\n", + " },\n", + " PRE_TRANSITIONS_PROCESSING: {\n", + " \"proc_name_1\": save_previous_node_response_to_ctx_processing\n", + " },\n", + " TRANSITIONS: {lbl.forward(0.1): cnd.true()},\n", + " },\n", + " \"flow\": {\n", + " \"step_0\": {RESPONSE: Message(text=\"first\")},\n", + " \"step_1\": {RESPONSE: Message(text=\"second\")},\n", + " \"step_2\": {RESPONSE: Message(text=\"third\")},\n", + " \"step_3\": {RESPONSE: Message(text=\"fourth\")},\n", + " \"step_4\": {RESPONSE: Message(text=\"fifth\")},\n", + " },\n", + "}\n", + "\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (Message(text=\"1\"), Message(text=\"previous=None: current=first\")),\n", + " (Message(text=\"2\"), Message(text=\"previous=first: current=second\")),\n", + " (Message(text=\"3\"), Message(text=\"previous=second: current=third\")),\n", + " (Message(text=\"4\"), Message(text=\"previous=third: current=fourth\")),\n", + " (Message(text=\"5\"), Message(text=\"previous=fourth: current=fifth\")),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f2951bfe", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:46.898453Z", + "iopub.status.busy": "2023-12-27T16:52:46.898074Z", + "iopub.status.idle": "2023-12-27T16:52:46.912874Z", + "shell.execute_reply": "2023-12-27T16:52:46.912250Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_22015/2767853180.py\", line 17, in get_previous_node_response_for_response_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_22015/2767853180.py\", line 17, in get_previous_node_response_for_response_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_22015/2767853180.py\", line 17, in get_previous_node_response_for_response_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_22015/2767853180.py\", line 17, in get_previous_node_response_for_response_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated for processing_name=proc_name_1 and processing_func=\n", + "Traceback (most recent call last):\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/normalization.py\", line 124, in processing_handler\n", + " ctx = processing_func(ctx, pipeline, *args, **kwargs)\n", + " File \"/tmp/ipykernel_22015/2767853180.py\", line 17, in get_previous_node_response_for_response_processing\n", + " ctx.overwrite_current_node_in_processing(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/dff/script/core/context.py\", line 292, in overwrite_current_node_in_processing\n", + " self.framework_states[\"actor\"][\"processed_node\"] = Node.model_validate(processed_node)\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/main.py\", line 503, in model_validate\n", + " return cls.__pydantic_validator__.validate_python(\n", + " File \"/home/runner/work/dialog_flow_framework/dialog_flow_framework/venv/lib/python3.9/site-packages/pydantic/_internal/_mock_val_ser.py\", line 47, in __getattr__\n", + " raise PydanticUserError(self._error_message, code=self._code)\n", + "pydantic.errors.PydanticUserError: Pydantic models should inherit from BaseModel, BaseModel cannot be instantiated directly\n", + "\n", + "For further information visit https://errors.pydantic.dev/2.5/u/base-model-instantiated\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='1'\n", + " (bot) <<< text='previous=None: current=first'\n", + "(user) >>> text='2'\n", + " (bot) <<< text='previous=first: current=second'\n", + "(user) >>> text='3'\n", + " (bot) <<< text='previous=second: current=third'\n", + "(user) >>> text='4'\n", + " (bot) <<< text='previous=third: current=fourth'\n", + "(user) >>> text='5'\n", + " (bot) <<< text='previous=fourth: current=fifth'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"root\", \"start\"),\n", + " fallback_label=(\"root\", \"fallback\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.responses.1_basics.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.1_basics.ipynb new file mode 100644 index 0000000000..5606ec860f --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.1_basics.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a3b41585", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Responses: 1. Basics\n", + "\n", + "Here, the process of response forming is shown.\n", + "Special keywords [RESPONSE](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords.RESPONSE)\n", + "and [TRANSITIONS](../apiref/dff.script.core.keywords.rst#dff.script.core.keywords.Keywords.TRANSITIONS)\n", + "are used for that." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a547ba09", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:48.763368Z", + "iopub.status.busy": "2023-12-27T16:52:48.763162Z", + "iopub.status.idle": "2023-12-27T16:52:51.112187Z", + "shell.execute_reply": "2023-12-27T16:52:51.111361Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a9c1bac3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:51.115182Z", + "iopub.status.busy": "2023-12-27T16:52:51.114972Z", + "iopub.status.idle": "2023-12-27T16:52:51.819352Z", + "shell.execute_reply": "2023-12-27T16:52:51.818712Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from typing import NamedTuple\n", + "\n", + "from dff.script import Message\n", + "from dff.script.conditions import exact_match\n", + "from dff.script import RESPONSE, TRANSITIONS\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c00174fc", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:51.822868Z", + "iopub.status.busy": "2023-12-27T16:52:51.822272Z", + "iopub.status.idle": "2023-12-27T16:52:51.830651Z", + "shell.execute_reply": "2023-12-27T16:52:51.830055Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"greeting_flow\": {\n", + " \"start_node\": {\n", + " RESPONSE: Message(text=\"\"),\n", + " TRANSITIONS: {\"node1\": exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " \"node1\": {\n", + " RESPONSE: Message(text=\"Hi, how are you?\"),\n", + " TRANSITIONS: {\n", + " \"node2\": exact_match(Message(text=\"i'm fine, how are you?\"))\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " \"node3\": exact_match(Message(text=\"Let's talk about music.\"))\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about music now.\"),\n", + " TRANSITIONS: {\"node4\": exact_match(Message(text=\"Ok, goodbye.\"))},\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"bye\"),\n", + " TRANSITIONS: {\"node1\": exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " \"fallback_node\": {\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {\"node1\": exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " }\n", + "}\n", + "\n", + "happy_path = (\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ),\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")),\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, how are you?\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"stop\"), Message(text=\"Ooops\")),\n", + " (Message(text=\"Hi\"), Message(text=\"Hi, how are you?\")),\n", + " (\n", + " Message(text=\"i'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ),\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about music now.\"),\n", + " ),\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5665a35", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:51.833023Z", + "iopub.status.busy": "2023-12-27T16:52:51.832822Z", + "iopub.status.idle": "2023-12-27T16:52:51.836044Z", + "shell.execute_reply": "2023-12-27T16:52:51.835282Z" + } + }, + "outputs": [], + "source": [ + "class CallbackRequest(NamedTuple):\n", + " payload: str" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "79194931", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:51.838635Z", + "iopub.status.busy": "2023-12-27T16:52:51.838254Z", + "iopub.status.idle": "2023-12-27T16:52:51.852382Z", + "shell.execute_reply": "2023-12-27T16:52:51.851668Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(\n", + " pipeline,\n", + " happy_path,\n", + " ) # This is a function for automatic tutorial running\n", + " # (testing) with `happy_path`\n", + "\n", + " # This runs tutorial in interactive mode if not in IPython env\n", + " # and if `DISABLE_INTERACTIVE_MODE` is not set\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline) # This runs tutorial in interactive mode" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.responses.2_buttons.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.2_buttons.ipynb new file mode 100644 index 0000000000..480273eef8 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.2_buttons.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d6869808", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "# Responses: 2. Buttons\n", + "\n", + "In this tutorial [Button](../apiref/dff.script.core.message.rst#dff.script.core.message.Button)\n", + "class is demonstrated.\n", + "Buttons are one of [Message](../apiref/dff.script.core.message.rst#dff.script.core.message.Message) fields.\n", + "They can be attached to any message but will only work if the chosen\n", + "[messenger interface](../apiref/index_messenger_interfaces.rst) supports them." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6b5c0108", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:53.470561Z", + "iopub.status.busy": "2023-12-27T16:52:53.470274Z", + "iopub.status.idle": "2023-12-27T16:52:55.920102Z", + "shell.execute_reply": "2023-12-27T16:52:55.919305Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "35e1839d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:55.923265Z", + "iopub.status.busy": "2023-12-27T16:52:55.922710Z", + "iopub.status.idle": "2023-12-27T16:52:56.632938Z", + "shell.execute_reply": "2023-12-27T16:52:56.632339Z" + } + }, + "outputs": [], + "source": [ + "import dff.script.conditions as cnd\n", + "import dff.script.labels as lbl\n", + "from dff.script import Context, TRANSITIONS, RESPONSE\n", + "\n", + "from dff.script.core.message import Button, Keyboard, Message\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ae47dca1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:56.636197Z", + "iopub.status.busy": "2023-12-27T16:52:56.635707Z", + "iopub.status.idle": "2023-12-27T16:52:56.639627Z", + "shell.execute_reply": "2023-12-27T16:52:56.638946Z" + } + }, + "outputs": [], + "source": [ + "def check_button_payload(value: str):\n", + " def payload_check_inner(ctx: Context, _: Pipeline):\n", + " if ctx.last_request.misc is not None:\n", + " return ctx.last_request.misc.get(\"payload\") == value\n", + " else:\n", + " return False\n", + "\n", + " return payload_check_inner" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5b245b2c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:56.642288Z", + "iopub.status.busy": "2023-12-27T16:52:56.641885Z", + "iopub.status.idle": "2023-12-27T16:52:56.657257Z", + "shell.execute_reply": "2023-12-27T16:52:56.656689Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"root\": {\n", + " \"start\": {\n", + " RESPONSE: Message(text=\"\"),\n", + " TRANSITIONS: {\n", + " (\"general\", \"question_1\"): cnd.true(),\n", + " },\n", + " },\n", + " \"fallback\": {RESPONSE: Message(text=\"Finishing test\")},\n", + " },\n", + " \"general\": {\n", + " \"question_1\": {\n", + " RESPONSE: Message(\n", + " **{\n", + " \"text\": \"Starting test! What's 2 + 2?\"\n", + " \" (type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"5\", payload=\"5\"),\n", + " Button(text=\"4\", payload=\"4\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): check_button_payload(\"4\"),\n", + " (\"general\", \"question_1\"): check_button_payload(\"5\"),\n", + " },\n", + " },\n", + " \"question_2\": {\n", + " RESPONSE: Message(\n", + " **{\n", + " \"text\": \"Next question: what's 6 * 8?\"\n", + " \" (type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"38\", payload=\"38\"),\n", + " Button(text=\"48\", payload=\"48\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): check_button_payload(\"48\"),\n", + " (\"general\", \"question_2\"): check_button_payload(\"38\"),\n", + " },\n", + " },\n", + " \"question_3\": {\n", + " RESPONSE: Message(\n", + " **{\n", + " \"text\": \"What's 114 + 115? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"229\", payload=\"229\"),\n", + " Button(text=\"283\", payload=\"283\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " TRANSITIONS: {\n", + " lbl.forward(): check_button_payload(\"229\"),\n", + " (\"general\", \"question_3\"): check_button_payload(\"283\"),\n", + " },\n", + " },\n", + " \"success\": {\n", + " RESPONSE: Message(text=\"Success!\"),\n", + " TRANSITIONS: {(\"root\", \"fallback\"): cnd.true()},\n", + " },\n", + " },\n", + "}\n", + "\n", + "happy_path = (\n", + " (\n", + " Message(text=\"Hi\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"Starting test! What's 2 + 2? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"5\", payload=\"5\"),\n", + " Button(text=\"4\", payload=\"4\"),\n", + " ]\n", + " )\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"0\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"Starting test! What's 2 + 2? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"5\", payload=\"5\"),\n", + " Button(text=\"4\", payload=\"4\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"1\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"Next question: what's 6 * 8? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"38\", payload=\"38\"),\n", + " Button(text=\"48\", payload=\"48\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"0\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"Next question: what's 6 * 8? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"38\", payload=\"38\"),\n", + " Button(text=\"48\", payload=\"48\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"1\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"What's 114 + 115? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"229\", payload=\"229\"),\n", + " Button(text=\"283\", payload=\"283\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"1\"),\n", + " Message(\n", + " **{\n", + " \"text\": \"What's 114 + 115? \"\n", + " \"(type in the index of the correct option)\",\n", + " \"misc\": {\n", + " \"ui\": Keyboard(\n", + " buttons=[\n", + " Button(text=\"229\", payload=\"229\"),\n", + " Button(text=\"283\", payload=\"283\"),\n", + " ]\n", + " ),\n", + " },\n", + " }\n", + " ),\n", + " ),\n", + " (Message(text=\"0\"), Message(text=\"Success!\")),\n", + " (Message(text=\"ok\"), Message(text=\"Finishing test\")),\n", + ")\n", + "\n", + "\n", + "def process_request(ctx: Context):\n", + " ui = (\n", + " ctx.last_response\n", + " and ctx.last_response.misc\n", + " and ctx.last_response.misc.get(\"ui\")\n", + " )\n", + " if ui and ui.buttons:\n", + " try:\n", + " chosen_button = ui.buttons[int(ctx.last_request.text)]\n", + " except (IndexError, ValueError):\n", + " raise ValueError(\n", + " \"Type in the index of the correct option \"\n", + " \"to choose from the buttons.\"\n", + " )\n", + " ctx.last_request = Message(misc={\"payload\": chosen_button.payload})" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4a4257ae", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:56.659814Z", + "iopub.status.busy": "2023-12-27T16:52:56.659366Z", + "iopub.status.idle": "2023-12-27T16:52:56.673478Z", + "shell.execute_reply": "2023-12-27T16:52:56.672851Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Starting test! What's 2 + 2? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '5', 'payload': '5'}, {'text': '4', 'payload': '4'}]}}'\n", + "(user) >>> text='0'\n", + " (bot) <<< text='Starting test! What's 2 + 2? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '5', 'payload': '5'}, {'text': '4', 'payload': '4'}]}}'\n", + "(user) >>> text='1'\n", + " (bot) <<< text='Next question: what's 6 * 8? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '38', 'payload': '38'}, {'text': '48', 'payload': '48'}]}}'\n", + "(user) >>> text='0'\n", + " (bot) <<< text='Next question: what's 6 * 8? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '38', 'payload': '38'}, {'text': '48', 'payload': '48'}]}}'\n", + "(user) >>> text='1'\n", + " (bot) <<< text='What's 114 + 115? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '229', 'payload': '229'}, {'text': '283', 'payload': '283'}]}}'\n", + "(user) >>> text='1'\n", + " (bot) <<< text='What's 114 + 115? (type in the index of the correct option)' misc='{'ui': {'buttons': [{'text': '229', 'payload': '229'}, {'text': '283', 'payload': '283'}]}}'\n", + "(user) >>> text='0'\n", + " (bot) <<< text='Success!'\n", + "(user) >>> text='ok'\n", + " (bot) <<< text='Finishing test'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"root\", \"start\"),\n", + " fallback_label=(\"root\", \"fallback\"),\n", + " pre_services=[process_request],\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(\n", + " pipeline,\n", + " happy_path,\n", + " ) # For response object with `happy_path` string comparing,\n", + " # a special `generics_comparer` comparator is used\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.responses.3_media.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.3_media.ipynb new file mode 100644 index 0000000000..054be85b77 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.3_media.ipynb @@ -0,0 +1,255 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5f120e70", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Responses: 3. Media\n", + "\n", + "Here, [Attachments](../apiref/dff.script.core.message.rst#dff.script.core.message.Attachments) class is shown.\n", + "Attachments can be used for attaching different media elements\n", + "(such as [Image](../apiref/dff.script.core.message.rst#dff.script.core.message.Image), [Document](../apiref/dff.script.core.message.rst#dff.script.core.message.Document)\n", + "or [Audio](../apiref/dff.script.core.message.rst#dff.script.core.message.Audio)).\n", + "\n", + "They can be attached to any message but will only work if the chosen\n", + "[messenger interface](../apiref/index_messenger_interfaces.rst) supports them." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "46891eba", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:52:58.219315Z", + "iopub.status.busy": "2023-12-27T16:52:58.218790Z", + "iopub.status.idle": "2023-12-27T16:53:00.634361Z", + "shell.execute_reply": "2023-12-27T16:53:00.633588Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bf7e9a6d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:00.637248Z", + "iopub.status.busy": "2023-12-27T16:53:00.637004Z", + "iopub.status.idle": "2023-12-27T16:53:01.353515Z", + "shell.execute_reply": "2023-12-27T16:53:01.352852Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from dff.script import RESPONSE, TRANSITIONS\n", + "from dff.script.conditions import std_conditions as cnd\n", + "\n", + "from dff.script.core.message import Attachments, Image, Message\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0864529c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:01.356830Z", + "iopub.status.busy": "2023-12-27T16:53:01.356318Z", + "iopub.status.idle": "2023-12-27T16:53:01.366895Z", + "shell.execute_reply": "2023-12-27T16:53:01.366321Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "img_url = \"https://www.python.org/static/img/python-logo.png\"\n", + "toy_script = {\n", + " \"root\": {\n", + " \"start\": {\n", + " RESPONSE: Message(text=\"\"),\n", + " TRANSITIONS: {(\"pics\", \"ask_picture\"): cnd.true()},\n", + " },\n", + " \"fallback\": {\n", + " RESPONSE: Message(\n", + " text=\"Final node reached, send any message to restart.\"\n", + " ),\n", + " TRANSITIONS: {(\"pics\", \"ask_picture\"): cnd.true()},\n", + " },\n", + " },\n", + " \"pics\": {\n", + " \"ask_picture\": {\n", + " RESPONSE: Message(text=\"Please, send me a picture url\"),\n", + " TRANSITIONS: {\n", + " (\"pics\", \"send_one\", 1.1): cnd.regexp(r\"^http.+\\.png$\"),\n", + " (\"pics\", \"send_many\", 1.0): cnd.regexp(\n", + " f\"{img_url} repeat 10 times\"\n", + " ),\n", + " (\"pics\", \"repeat\", 0.9): cnd.true(),\n", + " },\n", + " },\n", + " \"send_one\": {\n", + " RESPONSE: Message(\n", + " text=\"here's my picture!\",\n", + " attachments=Attachments(files=[Image(source=img_url)]),\n", + " ),\n", + " TRANSITIONS: {(\"root\", \"fallback\"): cnd.true()},\n", + " },\n", + " \"send_many\": {\n", + " RESPONSE: Message(\n", + " text=\"Look at my pictures\",\n", + " attachments=Attachments(files=[Image(source=img_url)] * 10),\n", + " ),\n", + " TRANSITIONS: {(\"root\", \"fallback\"): cnd.true()},\n", + " },\n", + " \"repeat\": {\n", + " RESPONSE: Message(\n", + " text=\"I cannot find the picture. Please, try again.\"\n", + " ),\n", + " TRANSITIONS: {\n", + " (\"pics\", \"send_one\", 1.1): cnd.regexp(r\"^http.+\\.png$\"),\n", + " (\"pics\", \"send_many\", 1.0): cnd.regexp(\n", + " r\"^http.+\\.png repeat 10 times\"\n", + " ),\n", + " (\"pics\", \"repeat\", 0.9): cnd.true(),\n", + " },\n", + " },\n", + " },\n", + "}\n", + "\n", + "happy_path = (\n", + " (Message(text=\"Hi\"), Message(text=\"Please, send me a picture url\")),\n", + " (\n", + " Message(text=\"no\"),\n", + " Message(text=\"I cannot find the picture. Please, try again.\"),\n", + " ),\n", + " (\n", + " Message(text=img_url),\n", + " Message(\n", + " text=\"here's my picture!\",\n", + " attachments=Attachments(files=[Image(source=img_url)]),\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"ok\"),\n", + " Message(text=\"Final node reached, send any message to restart.\"),\n", + " ),\n", + " (Message(text=\"ok\"), Message(text=\"Please, send me a picture url\")),\n", + " (\n", + " Message(text=f\"{img_url} repeat 10 times\"),\n", + " Message(\n", + " text=\"Look at my pictures\",\n", + " attachments=Attachments(files=[Image(source=img_url)] * 10),\n", + " ),\n", + " ),\n", + " (\n", + " Message(text=\"ok\"),\n", + " Message(text=\"Final node reached, send any message to restart.\"),\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "281aafe2", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:01.369490Z", + "iopub.status.busy": "2023-12-27T16:53:01.369100Z", + "iopub.status.idle": "2023-12-27T16:53:01.604233Z", + "shell.execute_reply": "2023-12-27T16:53:01.603575Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Please, send me a picture url'\n", + "(user) >>> text='no'\n", + " (bot) <<< text='I cannot find the picture. Please, try again.'\n", + "(user) >>> text='https://www.python.org/static/img/python-logo.png'\n", + " (bot) <<< text='here's my picture!' attachments='{'files': [{'source': Url('https://www.python.org/static/img/python-logo.png')}]}'\n", + "(user) >>> text='ok'\n", + " (bot) <<< text='Final node reached, send any message to restart.'\n", + "(user) >>> text='ok'\n", + " (bot) <<< text='Please, send me a picture url'\n", + "(user) >>> text='https://www.python.org/static/img/python-logo.png repeat 10 times'\n", + " (bot) <<< text='Look at my pictures' attachments='{'files': [{'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}, {'source': Url('https://www.python.org/static/img/python-logo.png')}]}'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='ok'\n", + " (bot) <<< text='Final node reached, send any message to restart.'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"root\", \"start\"),\n", + " fallback_label=(\"root\", \"fallback\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.script.responses.4_multi_message.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.4_multi_message.ipynb new file mode 100644 index 0000000000..01fa24be29 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.script.responses.4_multi_message.ipynb @@ -0,0 +1,283 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d9405510", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# Responses: 4. Multi Message\n", + "\n", + "This tutorial shows Multi Message usage.\n", + "\n", + "The [MultiMessage](../apiref/dff.script.core.message.rst#dff.script.core.message.MultiMessage) represents a combination of several messages.\n", + "\n", + "Let's do all the necessary imports from DFF." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f7859c2c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:03.399923Z", + "iopub.status.busy": "2023-12-27T16:53:03.399471Z", + "iopub.status.idle": "2023-12-27T16:53:05.884629Z", + "shell.execute_reply": "2023-12-27T16:53:05.883682Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3d4f2e50", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:05.890653Z", + "iopub.status.busy": "2023-12-27T16:53:05.890056Z", + "iopub.status.idle": "2023-12-27T16:53:06.598434Z", + "shell.execute_reply": "2023-12-27T16:53:06.597783Z" + } + }, + "outputs": [], + "source": [ + "\n", + "from dff.script import TRANSITIONS, RESPONSE, Message, MultiMessage\n", + "import dff.script.conditions as cnd\n", + "\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8e5992a7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:06.602155Z", + "iopub.status.busy": "2023-12-27T16:53:06.601709Z", + "iopub.status.idle": "2023-12-27T16:53:06.614547Z", + "shell.execute_reply": "2023-12-27T16:53:06.613595Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"greeting_flow\": {\n", + " \"start_node\": { # This is an initial node,\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " # If \"Hi\" == request of user then we make the transition\n", + " },\n", + " \"node1\": {\n", + " RESPONSE: MultiMessage(\n", + " messages=[\n", + " Message(text=\"Hi, what is up?\", misc={\"confidences\": 0.85}),\n", + " Message(\n", + " text=\"Hello, how are you?\", misc={\"confidences\": 0.9}\n", + " ),\n", + " ]\n", + " ),\n", + " TRANSITIONS: {\n", + " \"node2\": cnd.exact_match(Message(text=\"I'm fine, how are you?\"))\n", + " },\n", + " },\n", + " \"node2\": {\n", + " RESPONSE: Message(text=\"Good. What do you want to talk about?\"),\n", + " TRANSITIONS: {\n", + " \"node3\": cnd.exact_match(\n", + " Message(text=\"Let's talk about music.\")\n", + " )\n", + " },\n", + " },\n", + " \"node3\": {\n", + " RESPONSE: Message(text=\"Sorry, I can not talk about that now.\"),\n", + " TRANSITIONS: {\n", + " \"node4\": cnd.exact_match(Message(text=\"Ok, goodbye.\"))\n", + " },\n", + " },\n", + " \"node4\": {\n", + " RESPONSE: Message(text=\"bye\"),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " \"fallback_node\": { # We get to this node\n", + " # if an error occurred while the agent was running.\n", + " RESPONSE: Message(text=\"Ooops\"),\n", + " TRANSITIONS: {\"node1\": cnd.exact_match(Message(text=\"Hi\"))},\n", + " },\n", + " }\n", + "}\n", + "\n", + "# testing\n", + "happy_path = (\n", + " (\n", + " Message(text=\"Hi\"),\n", + " MultiMessage(\n", + " messages=[\n", + " Message(text=\"Hi, what is up?\", misc={\"confidences\": 0.85}),\n", + " Message(text=\"Hello, how are you?\", misc={\"confidences\": 0.9}),\n", + " ]\n", + " ),\n", + " ), # start_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about that now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")), # node3 -> node4\n", + " (\n", + " Message(text=\"Hi\"),\n", + " MultiMessage(\n", + " messages=[\n", + " Message(text=\"Hi, what is up?\", misc={\"confidences\": 0.85}),\n", + " Message(text=\"Hello, how are you?\", misc={\"confidences\": 0.9}),\n", + " ]\n", + " ),\n", + " ), # node4 -> node1\n", + " (\n", + " Message(text=\"stop\"),\n", + " Message(text=\"Ooops\"),\n", + " ),\n", + " # node1 -> fallback_node\n", + " (\n", + " Message(text=\"one\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"help\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"nope\"),\n", + " Message(text=\"Ooops\"),\n", + " ), # f_n->f_n\n", + " (\n", + " Message(text=\"Hi\"),\n", + " MultiMessage(\n", + " messages=[\n", + " Message(text=\"Hi, what is up?\", misc={\"confidences\": 0.85}),\n", + " Message(text=\"Hello, how are you?\", misc={\"confidences\": 0.9}),\n", + " ]\n", + " ),\n", + " ), # fallback_node -> node1\n", + " (\n", + " Message(text=\"I'm fine, how are you?\"),\n", + " Message(text=\"Good. What do you want to talk about?\"),\n", + " ), # node1 -> node2\n", + " (\n", + " Message(text=\"Let's talk about music.\"),\n", + " Message(text=\"Sorry, I can not talk about that now.\"),\n", + " ), # node2 -> node3\n", + " (Message(text=\"Ok, goodbye.\"), Message(text=\"bye\")), # node3 -> node4\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "51cbfe74", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:06.618041Z", + "iopub.status.busy": "2023-12-27T16:53:06.617543Z", + "iopub.status.idle": "2023-12-27T16:53:06.634990Z", + "shell.execute_reply": "2023-12-27T16:53:06.633937Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< messages='[{'text': 'Hi, what is up?', 'misc': {'confidences': 0.85}}, {'text': 'Hello, how are you?', 'misc': {'confidences': 0.9}}]'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about that now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< messages='[{'text': 'Hi, what is up?', 'misc': {'confidences': 0.85}}, {'text': 'Hello, how are you?', 'misc': {'confidences': 0.9}}]'\n", + "(user) >>> text='stop'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='one'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='help'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='nope'\n", + " (bot) <<< text='Ooops'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< messages='[{'text': 'Hi, what is up?', 'misc': {'confidences': 0.85}}, {'text': 'Hello, how are you?', 'misc': {'confidences': 0.9}}]'\n", + "(user) >>> text='I'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about that now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n" + ] + } + ], + "source": [ + "\n", + "pipeline = Pipeline.from_script(\n", + " toy_script,\n", + " start_label=(\"greeting_flow\", \"start_node\"),\n", + " fallback_label=(\"greeting_flow\", \"fallback_node\"),\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.stats.1_extractor_functions.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.stats.1_extractor_functions.ipynb new file mode 100644 index 0000000000..8fb0c5e421 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.stats.1_extractor_functions.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1c5b35be", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 1. Extractor Functions\n", + "\n", + "The following example covers the basics of using the `stats` module.\n", + "\n", + "Statistics are collected from pipeline services by extractor functions\n", + "that report the state of one or more pipeline components. The `stats` module\n", + "provides several default extractors, but users are free to define their own\n", + "extractor functions. You can find API reference for default extractors\n", + "[here](../apiref/dff.stats.default_extractors.rst).\n", + "\n", + "It is a preferred practice to define extractors as asynchronous functions.\n", + "Extractors need to have the following uniform signature:\n", + "the expected arguments are always `Context`, `Pipeline`, and `ExtraHandlerRuntimeInfo`,\n", + "while the expected return value is an arbitrary `dict` or a `None`.\n", + "The returned value gets persisted to Clickhouse as JSON\n", + "which is why it can contain arbitrarily nested dictionaries,\n", + "but it cannot contain any complex objects that cannot be trivially serialized.\n", + "\n", + "The output of these functions will be captured by an OpenTelemetry instrumentor and directed to\n", + "the Opentelemetry collector server which in its turn batches and persists data\n", + "to Clickhouse or other OLAP storages.\n", + "\n", + "
\n", + "\n", + "Both the Opentelemetry collector and the Clickhouse instance must be running\n", + "during statistics collection. If you cloned the DFF repo, launch them using `docker compose`:\n", + "```bash\n", + "docker compose --profile stats up\n", + "```\n", + "\n", + "
\n", + "\n", + "For more information on OpenTelemetry instrumentation,\n", + "refer to the body of this tutorial as well as [OpenTelemetry documentation](\n", + "https://opentelemetry.io/docs/instrumentation/python/manual/\n", + ")." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3eb07780", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:08.459081Z", + "iopub.status.busy": "2023-12-27T16:53:08.458459Z", + "iopub.status.idle": "2023-12-27T16:53:10.882397Z", + "shell.execute_reply": "2023-12-27T16:53:10.881517Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[stats]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7d37d0a3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:10.887073Z", + "iopub.status.busy": "2023-12-27T16:53:10.886832Z", + "iopub.status.idle": "2023-12-27T16:53:11.928555Z", + "shell.execute_reply": "2023-12-27T16:53:11.927943Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import asyncio\n", + "\n", + "from dff.script import Context\n", + "from dff.pipeline import (\n", + " Pipeline,\n", + " ACTOR,\n", + " Service,\n", + " ExtraHandlerRuntimeInfo,\n", + " to_service,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH\n", + "from dff.stats import OtelInstrumentor, default_extractors\n", + "from dff.utils.testing import is_interactive_mode, check_happy_path" + ] + }, + { + "cell_type": "markdown", + "id": "9798a6dc", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "The cells below configure log export with the help of OTLP instrumentation.\n", + "\n", + "* The initial step is to configure the export destination.\n", + "`from_url` method of the `OtelInstrumentor` class simplifies this task\n", + "allowing you to only pass the url of the OTLP Collector server.\n", + "\n", + "* Alternatively, you can use the utility functions provided by the `stats` module:\n", + "`set_logger_destination`, `set_tracer_destination`, or `set_meter_destination`. These accept\n", + "an appropriate Opentelemetry exporter instance and bind it to provider classes.\n", + "\n", + "* Nextly, the `OtelInstrumentor` class should be constructed to log the output\n", + "of extractor functions. Custom extractors need to be decorated\n", + "with the `OtelInstrumentor` instance. Default extractors are instrumented\n", + "by calling the `instrument` method.\n", + "\n", + "* The entirety of the process is illustrated in the example below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "53b4697c", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:11.931785Z", + "iopub.status.busy": "2023-12-27T16:53:11.931237Z", + "iopub.status.idle": "2023-12-27T16:53:11.938322Z", + "shell.execute_reply": "2023-12-27T16:53:11.937755Z" + } + }, + "outputs": [], + "source": [ + "dff_instrumentor = OtelInstrumentor.from_url(\"grpc://localhost:4317\")\n", + "dff_instrumentor.instrument()" + ] + }, + { + "cell_type": "markdown", + "id": "c157f24f", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 2 + }, + "source": [ + "The following cell shows a custom extractor function. The data obtained from\n", + "the context and the runtime information gets shaped as a dict and returned\n", + "from the function body. The `dff_instrumentor` decorator then ensures\n", + "that the output is logged by OpenTelemetry." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "11f2b6bb", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:11.940812Z", + "iopub.status.busy": "2023-12-27T16:53:11.940604Z", + "iopub.status.idle": "2023-12-27T16:53:11.944133Z", + "shell.execute_reply": "2023-12-27T16:53:11.943563Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# decorated by an OTLP Instrumentor instance\n", + "@dff_instrumentor\n", + "async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo):\n", + " # extract the execution state of a target service\n", + " data = {\n", + " \"execution_state\": info.component.execution_state,\n", + " }\n", + " # return the state as an arbitrary dict for further logging\n", + " return data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4f6aa926", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:11.946540Z", + "iopub.status.busy": "2023-12-27T16:53:11.946189Z", + "iopub.status.idle": "2023-12-27T16:53:11.949727Z", + "shell.execute_reply": "2023-12-27T16:53:11.949172Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# configure `get_service_state` to run after the `heavy_service`\n", + "@to_service(after_handler=[get_service_state])\n", + "async def heavy_service(ctx: Context):\n", + " _ = ctx # get something from ctx if needed\n", + " await asyncio.sleep(0.02)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "08d14e60", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:11.952442Z", + "iopub.status.busy": "2023-12-27T16:53:11.952032Z", + "iopub.status.idle": "2023-12-27T16:53:12.089224Z", + "shell.execute_reply": "2023-12-27T16:53:12.088514Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(\n", + " {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " heavy_service,\n", + " Service(\n", + " handler=ACTOR,\n", + " after_handler=[default_extractors.get_current_label],\n", + " ),\n", + " ],\n", + " }\n", + ")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " pipeline.run()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.stats.2_pipeline_integration.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.stats.2_pipeline_integration.ipynb new file mode 100644 index 0000000000..7ffb9720f4 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.stats.2_pipeline_integration.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e4f2277a", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 2. Pipeline Integration\n", + "\n", + "In the DFF ecosystem, extractor functions act as regular extra handlers (\n", + "[see the pipeline module documentation](../tutorials/tutorials.pipeline.6_extra_handlers_basic.py)\n", + ").\n", + "Hence, you can decorate any part of your pipeline, including services,\n", + "service groups and the pipeline as a whole, to obtain the statistics\n", + "specific for that component. Some examples of this functionality\n", + "are showcased in this tutorial.\n", + "\n", + "
\n", + "\n", + "Both the Opentelemetry collector and the Clickhouse instance must be running\n", + "during statistics collection. If you cloned the DFF repo, launch them using `docker compose`:\n", + "```bash\n", + "docker compose --profile stats up\n", + "```\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ff3a77f1", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:13.867361Z", + "iopub.status.busy": "2023-12-27T16:53:13.867155Z", + "iopub.status.idle": "2023-12-27T16:53:16.326055Z", + "shell.execute_reply": "2023-12-27T16:53:16.325272Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff[stats]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d8ab55ca", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:16.329088Z", + "iopub.status.busy": "2023-12-27T16:53:16.328676Z", + "iopub.status.idle": "2023-12-27T16:53:17.256485Z", + "shell.execute_reply": "2023-12-27T16:53:17.255565Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "\n", + "from dff.script import Context\n", + "from dff.pipeline import (\n", + " Pipeline,\n", + " ACTOR,\n", + " Service,\n", + " ExtraHandlerRuntimeInfo,\n", + " ServiceGroup,\n", + " GlobalExtraHandlerType,\n", + ")\n", + "from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH\n", + "from dff.stats import (\n", + " OtelInstrumentor,\n", + " set_logger_destination,\n", + " set_tracer_destination,\n", + ")\n", + "from dff.stats import OTLPLogExporter, OTLPSpanExporter\n", + "from dff.stats import default_extractors\n", + "from dff.utils.testing import is_interactive_mode, check_happy_path" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "399fbb5e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:17.260696Z", + "iopub.status.busy": "2023-12-27T16:53:17.260099Z", + "iopub.status.idle": "2023-12-27T16:53:17.269992Z", + "shell.execute_reply": "2023-12-27T16:53:17.269084Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "set_logger_destination(OTLPLogExporter(\"grpc://localhost:4317\", insecure=True))\n", + "set_tracer_destination(OTLPSpanExporter(\"grpc://localhost:4317\", insecure=True))\n", + "dff_instrumentor = OtelInstrumentor()\n", + "dff_instrumentor.instrument()\n", + "\n", + "\n", + "# example extractor function\n", + "@dff_instrumentor\n", + "async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo):\n", + " # extract execution state of service from info\n", + " data = {\n", + " \"execution_state\": info.component.execution_state,\n", + " }\n", + " # return a record to save into connected database\n", + " return data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ee37ddd3", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:17.274255Z", + "iopub.status.busy": "2023-12-27T16:53:17.273833Z", + "iopub.status.idle": "2023-12-27T16:53:17.277584Z", + "shell.execute_reply": "2023-12-27T16:53:17.276815Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# example service\n", + "async def heavy_service(ctx: Context):\n", + " _ = ctx # get something from ctx if needed\n", + " await asyncio.sleep(0.02)" + ] + }, + { + "cell_type": "markdown", + "id": "b6c75dc1", + "metadata": { + "cell_marker": "\"\"\"", + "lines_to_next_cell": 0 + }, + "source": [ + "\n", + "The many ways in which you can use extractor functions are shown in the following\n", + "pipeline definition. The functions are used to obtain statistics from respective components:\n", + "\n", + "* A service group of two `heavy_service` instances.\n", + "* An `Actor` service.\n", + "* The pipeline as a whole.\n", + "\n", + "As is the case with the regular extra handler functions, you can wire the extractors\n", + "to run either before or after the target service. As a result, you can compare\n", + "the pre-service and post-service states of the context to measure the performance\n", + "of various components, etc.\n", + "\n", + "Some extractors, like `get_current_label`, have restrictions in terms of their run stage:\n", + "for instance, `get_current_label` needs to only be used as an `after_handler`\n", + "to function correctly." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "94843910", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:17.281001Z", + "iopub.status.busy": "2023-12-27T16:53:17.280661Z", + "iopub.status.idle": "2023-12-27T16:53:17.434405Z", + "shell.execute_reply": "2023-12-27T16:53:17.433535Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n", + "(user) >>> text='i'm fine, how are you?'\n", + " (bot) <<< text='Good. What do you want to talk about?'\n", + "(user) >>> text='Let's talk about music.'\n", + " (bot) <<< text='Sorry, I can not talk about music now.'\n", + "(user) >>> text='Ok, goodbye.'\n", + " (bot) <<< text='bye'\n", + "(user) >>> text='Hi'\n", + " (bot) <<< text='Hi, how are you?'\n" + ] + } + ], + "source": [ + "pipeline = Pipeline.from_dict(\n", + " {\n", + " \"script\": TOY_SCRIPT,\n", + " \"start_label\": (\"greeting_flow\", \"start_node\"),\n", + " \"fallback_label\": (\"greeting_flow\", \"fallback_node\"),\n", + " \"components\": [\n", + " ServiceGroup(\n", + " before_handler=[default_extractors.get_timing_before],\n", + " after_handler=[\n", + " get_service_state,\n", + " default_extractors.get_timing_after,\n", + " ],\n", + " components=[\n", + " {\"handler\": heavy_service},\n", + " {\"handler\": heavy_service},\n", + " ],\n", + " ),\n", + " Service(\n", + " handler=ACTOR,\n", + " before_handler=[\n", + " default_extractors.get_timing_before,\n", + " ],\n", + " after_handler=[\n", + " get_service_state,\n", + " default_extractors.get_current_label,\n", + " default_extractors.get_timing_after,\n", + " ],\n", + " ),\n", + " ],\n", + " }\n", + ")\n", + "pipeline.add_global_handler(\n", + " GlobalExtraHandlerType.BEFORE_ALL, default_extractors.get_timing_before\n", + ")\n", + "pipeline.add_global_handler(\n", + " GlobalExtraHandlerType.AFTER_ALL, default_extractors.get_timing_after\n", + ")\n", + "pipeline.add_global_handler(GlobalExtraHandlerType.AFTER_ALL, get_service_state)\n", + "\n", + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, HAPPY_PATH)\n", + " if is_interactive_mode():\n", + " pipeline.run()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.utils.1_cache.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.utils.1_cache.ipynb new file mode 100644 index 0000000000..f606db4973 --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.utils.1_cache.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4fd91d1e", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 1. Cache\n", + "\n", + "In this tutorial use of\n", + "[cache](../apiref/dff.utils.turn_caching.singleton_turn_caching.rst#dff.utils.turn_caching.singleton_turn_caching.cache)\n", + "function is demonstrated.\n", + "\n", + "This function is used a lot like `functools.cache` function and\n", + "helps by saving results of heavy function execution and avoiding recalculation.\n", + "\n", + "Caches are kept in a library-wide singleton\n", + "and are cleared at the end of each turn." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9493759e", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:19.386830Z", + "iopub.status.busy": "2023-12-27T16:53:19.386254Z", + "iopub.status.idle": "2023-12-27T16:53:21.711846Z", + "shell.execute_reply": "2023-12-27T16:53:21.711042Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "943dc282", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:21.714871Z", + "iopub.status.busy": "2023-12-27T16:53:21.714433Z", + "iopub.status.idle": "2023-12-27T16:53:22.429706Z", + "shell.execute_reply": "2023-12-27T16:53:22.429022Z" + } + }, + "outputs": [], + "source": [ + "from dff.script.conditions import true\n", + "from dff.script import Context, TRANSITIONS, RESPONSE, Message\n", + "from dff.script.labels import repeat\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.turn_caching import cache\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "\n", + "\n", + "external_data = {\"counter\": 0}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "11e8668d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:22.432913Z", + "iopub.status.busy": "2023-12-27T16:53:22.432388Z", + "iopub.status.idle": "2023-12-27T16:53:22.437139Z", + "shell.execute_reply": "2023-12-27T16:53:22.436491Z" + } + }, + "outputs": [], + "source": [ + "@cache\n", + "def cached_response(_):\n", + " \"\"\"\n", + " This function execution result will be saved\n", + " for any set of given argument(s).\n", + " If the function will be called again\n", + " with the same arguments it will prevent it from execution.\n", + " The cached values will be used instead.\n", + " The cache is stored in a library-wide singleton,\n", + " that is cleared in the end of execution of actor and/or pipeline.\n", + " \"\"\"\n", + " external_data[\"counter\"] += 1\n", + " return external_data[\"counter\"]\n", + "\n", + "\n", + "def response(ctx: Context, _, *__, **___) -> Message:\n", + " if ctx.validation:\n", + " return Message()\n", + " return Message(\n", + " text=f\"{cached_response(1)}-{cached_response(2)}-\"\n", + " f\"{cached_response(1)}-{cached_response(2)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7c5243ca", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:22.439685Z", + "iopub.status.busy": "2023-12-27T16:53:22.439300Z", + "iopub.status.idle": "2023-12-27T16:53:22.443899Z", + "shell.execute_reply": "2023-12-27T16:53:22.443255Z" + }, + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"flow\": {\"node1\": {TRANSITIONS: {repeat(): true()}, RESPONSE: response}}\n", + "}\n", + "\n", + "happy_path = (\n", + " (Message(), Message(text=\"1-2-1-2\")),\n", + " (Message(), Message(text=\"3-4-3-4\")),\n", + " (Message(), Message(text=\"5-6-5-6\")),\n", + ")\n", + "\n", + "pipeline = Pipeline.from_script(toy_script, start_label=(\"flow\", \"node1\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d77da0b9", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:22.446454Z", + "iopub.status.busy": "2023-12-27T16:53:22.446005Z", + "iopub.status.idle": "2023-12-27T16:53:22.452250Z", + "shell.execute_reply": "2023-12-27T16:53:22.451488Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> \n", + " (bot) <<< text='1-2-1-2'\n", + "(user) >>> \n", + " (bot) <<< text='3-4-3-4'\n", + "(user) >>> \n", + " (bot) <<< text='5-6-5-6'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/tutorials/tutorials.utils.2_lru_cache.ipynb b/.doctrees/nbsphinx/tutorials/tutorials.utils.2_lru_cache.ipynb new file mode 100644 index 0000000000..bde28fed4e --- /dev/null +++ b/.doctrees/nbsphinx/tutorials/tutorials.utils.2_lru_cache.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "92448c7b", + "metadata": { + "cell_marker": "\"\"\"" + }, + "source": [ + "# 2. LRU Cache\n", + "\n", + "In this tutorial use of\n", + "[lru_cache](../apiref/dff.utils.turn_caching.singleton_turn_caching.rst#dff.utils.turn_caching.singleton_turn_caching.lru_cache)\n", + "function is demonstrated.\n", + "\n", + "This function is used a lot like `functools.lru_cache` function and\n", + "helps by saving results of heavy function execution and avoiding recalculation.\n", + "\n", + "Caches are kept in a library-wide singleton\n", + "and are cleared at the end of each turn.\n", + "\n", + "Maximum size parameter limits the amount of function execution results cached." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a5bb5a87", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:24.319019Z", + "iopub.status.busy": "2023-12-27T16:53:24.318807Z", + "iopub.status.idle": "2023-12-27T16:53:26.629349Z", + "shell.execute_reply": "2023-12-27T16:53:26.628552Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# installing dependencies\n", + "%pip install -q dff" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "41cb77a7", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:26.632777Z", + "iopub.status.busy": "2023-12-27T16:53:26.632088Z", + "iopub.status.idle": "2023-12-27T16:53:27.360487Z", + "shell.execute_reply": "2023-12-27T16:53:27.359828Z" + } + }, + "outputs": [], + "source": [ + "from dff.script.conditions import true\n", + "from dff.script import Context, TRANSITIONS, RESPONSE, Message\n", + "from dff.script.labels import repeat\n", + "from dff.pipeline import Pipeline\n", + "from dff.utils.turn_caching import lru_cache\n", + "from dff.utils.testing.common import (\n", + " check_happy_path,\n", + " is_interactive_mode,\n", + " run_interactive_mode,\n", + ")\n", + "\n", + "external_data = {\"counter\": 0}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4ec0b5b4", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:27.363462Z", + "iopub.status.busy": "2023-12-27T16:53:27.363151Z", + "iopub.status.idle": "2023-12-27T16:53:27.368293Z", + "shell.execute_reply": "2023-12-27T16:53:27.367582Z" + } + }, + "outputs": [], + "source": [ + "@lru_cache(maxsize=2)\n", + "def cached_response(_):\n", + " \"\"\"\n", + " This function will work exactly the same as the one from previous\n", + " tutorial with only one exception.\n", + " Only 2 results will be stored;\n", + " when the function will be executed with third arguments set,\n", + " the least recent result will be deleted.\n", + " \"\"\"\n", + " external_data[\"counter\"] += 1\n", + " return external_data[\"counter\"]\n", + "\n", + "\n", + "def response(ctx: Context, _, *__, **___) -> Message:\n", + " if ctx.validation:\n", + " return Message()\n", + " return Message(\n", + " text=f\"{cached_response(1)}-{cached_response(2)}-{cached_response(3)}-\"\n", + " f\"{cached_response(2)}-{cached_response(1)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e60e680d", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:27.370902Z", + "iopub.status.busy": "2023-12-27T16:53:27.370460Z", + "iopub.status.idle": "2023-12-27T16:53:27.374957Z", + "shell.execute_reply": "2023-12-27T16:53:27.374328Z" + } + }, + "outputs": [], + "source": [ + "toy_script = {\n", + " \"flow\": {\"node1\": {TRANSITIONS: {repeat(): true()}, RESPONSE: response}}\n", + "}\n", + "\n", + "happy_path = (\n", + " (Message(), Message(text=\"1-2-3-2-4\")),\n", + " (Message(), Message(text=\"5-6-7-6-8\")),\n", + " (Message(), Message(text=\"9-10-11-10-12\")),\n", + ")\n", + "\n", + "pipeline = Pipeline.from_script(toy_script, start_label=(\"flow\", \"node1\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "73131c4f", + "metadata": { + "execution": { + "iopub.execute_input": "2023-12-27T16:53:27.377583Z", + "iopub.status.busy": "2023-12-27T16:53:27.377229Z", + "iopub.status.idle": "2023-12-27T16:53:27.383417Z", + "shell.execute_reply": "2023-12-27T16:53:27.382750Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(user) >>> \n", + " (bot) <<< text='1-2-3-2-4'\n", + "(user) >>> \n", + " (bot) <<< text='5-6-7-6-8'\n", + "(user) >>> \n", + " (bot) <<< text='9-10-11-10-12'\n" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " check_happy_path(pipeline, happy_path)\n", + " if is_interactive_mode():\n", + " run_interactive_mode(pipeline)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all", + "text_representation": { + "extension": ".py", + "format_name": "percent" + } + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/reference.doctree b/.doctrees/reference.doctree new file mode 100644 index 0000000000..a7015a7626 Binary files /dev/null and b/.doctrees/reference.doctree differ diff --git a/.doctrees/tutorials.doctree b/.doctrees/tutorials.doctree new file mode 100644 index 0000000000..ba45e21adf Binary files /dev/null and b/.doctrees/tutorials.doctree differ diff --git a/.doctrees/tutorials/index_context_storages.doctree b/.doctrees/tutorials/index_context_storages.doctree new file mode 100644 index 0000000000..039bf67027 Binary files /dev/null and b/.doctrees/tutorials/index_context_storages.doctree differ diff --git a/.doctrees/tutorials/index_interfaces.doctree b/.doctrees/tutorials/index_interfaces.doctree new file mode 100644 index 0000000000..e1460570cd Binary files /dev/null and b/.doctrees/tutorials/index_interfaces.doctree differ diff --git a/.doctrees/tutorials/index_pipeline.doctree b/.doctrees/tutorials/index_pipeline.doctree new file mode 100644 index 0000000000..844aa85905 Binary files /dev/null and b/.doctrees/tutorials/index_pipeline.doctree differ diff --git a/.doctrees/tutorials/index_script.doctree b/.doctrees/tutorials/index_script.doctree new file mode 100644 index 0000000000..3a9342f810 Binary files /dev/null and b/.doctrees/tutorials/index_script.doctree differ diff --git a/.doctrees/tutorials/index_stats.doctree b/.doctrees/tutorials/index_stats.doctree new file mode 100644 index 0000000000..ad36fb289a Binary files /dev/null and b/.doctrees/tutorials/index_stats.doctree differ diff --git a/.doctrees/tutorials/index_utils.doctree b/.doctrees/tutorials/index_utils.doctree new file mode 100644 index 0000000000..1c17ff7b28 Binary files /dev/null and b/.doctrees/tutorials/index_utils.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.1_basics.doctree b/.doctrees/tutorials/tutorials.context_storages.1_basics.doctree new file mode 100644 index 0000000000..ae68fc3eb8 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.1_basics.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.2_postgresql.doctree b/.doctrees/tutorials/tutorials.context_storages.2_postgresql.doctree new file mode 100644 index 0000000000..21eb7065e2 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.2_postgresql.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.3_mongodb.doctree b/.doctrees/tutorials/tutorials.context_storages.3_mongodb.doctree new file mode 100644 index 0000000000..090accf07a Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.3_mongodb.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.4_redis.doctree b/.doctrees/tutorials/tutorials.context_storages.4_redis.doctree new file mode 100644 index 0000000000..401c03a8f4 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.4_redis.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.5_mysql.doctree b/.doctrees/tutorials/tutorials.context_storages.5_mysql.doctree new file mode 100644 index 0000000000..85bc61577e Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.5_mysql.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.6_sqlite.doctree b/.doctrees/tutorials/tutorials.context_storages.6_sqlite.doctree new file mode 100644 index 0000000000..0dcf9ebd28 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.6_sqlite.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.7_yandex_database.doctree b/.doctrees/tutorials/tutorials.context_storages.7_yandex_database.doctree new file mode 100644 index 0000000000..fe92fdda25 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.7_yandex_database.doctree differ diff --git a/.doctrees/tutorials/tutorials.context_storages.8_db_benchmarking.doctree b/.doctrees/tutorials/tutorials.context_storages.8_db_benchmarking.doctree new file mode 100644 index 0000000000..11f75f1943 Binary files /dev/null and b/.doctrees/tutorials/tutorials.context_storages.8_db_benchmarking.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.1_basic.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.1_basic.doctree new file mode 100644 index 0000000000..6d814fcc00 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.1_basic.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.2_buttons.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.2_buttons.doctree new file mode 100644 index 0000000000..14dcedf8c1 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.2_buttons.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.doctree new file mode 100644 index 0000000000..0f79e4c2c2 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.4_conditions.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.4_conditions.doctree new file mode 100644 index 0000000000..1507a1c1da Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.4_conditions.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.5_conditions_with_media.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.5_conditions_with_media.doctree new file mode 100644 index 0000000000..2bfc627c2d Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.5_conditions_with_media.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.6_conditions_extras.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.6_conditions_extras.doctree new file mode 100644 index 0000000000..6ea4be5d66 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.6_conditions_extras.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.7_polling_setup.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.7_polling_setup.doctree new file mode 100644 index 0000000000..381d634b00 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.7_polling_setup.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.telegram.8_webhook_setup.doctree b/.doctrees/tutorials/tutorials.messengers.telegram.8_webhook_setup.doctree new file mode 100644 index 0000000000..92cd0c3cdf Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.telegram.8_webhook_setup.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.web_api_interface.1_fastapi.doctree b/.doctrees/tutorials/tutorials.messengers.web_api_interface.1_fastapi.doctree new file mode 100644 index 0000000000..722ad3cd6a Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.web_api_interface.1_fastapi.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.doctree b/.doctrees/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.doctree new file mode 100644 index 0000000000..2e65c160c3 Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.doctree b/.doctrees/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.doctree new file mode 100644 index 0000000000..6f30eb992a Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.doctree differ diff --git a/.doctrees/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.doctree b/.doctrees/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.doctree new file mode 100644 index 0000000000..fc2c1a574c Binary files /dev/null and b/.doctrees/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.1_basics.doctree b/.doctrees/tutorials/tutorials.pipeline.1_basics.doctree new file mode 100644 index 0000000000..120b22b9bf Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.1_basics.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.2_pre_and_post_processors.doctree b/.doctrees/tutorials/tutorials.pipeline.2_pre_and_post_processors.doctree new file mode 100644 index 0000000000..80f80c02ba Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.2_pre_and_post_processors.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.doctree b/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.doctree new file mode 100644 index 0000000000..eb2d7158c5 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.doctree b/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.doctree new file mode 100644 index 0000000000..f91d8b76a9 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.doctree b/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.doctree new file mode 100644 index 0000000000..a727dd80bc Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_full.doctree b/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_full.doctree new file mode 100644 index 0000000000..36a1201cf2 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.4_groups_and_conditions_full.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.doctree b/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.doctree new file mode 100644 index 0000000000..b371e29596 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.doctree b/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.doctree new file mode 100644 index 0000000000..6fca3f1e12 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_basic.doctree b/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_basic.doctree new file mode 100644 index 0000000000..129e0f8e91 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_basic.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_full.doctree b/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_full.doctree new file mode 100644 index 0000000000..e82f9c6024 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.6_extra_handlers_full.doctree differ diff --git a/.doctrees/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.doctree b/.doctrees/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.doctree new file mode 100644 index 0000000000..e33ca0a0f8 Binary files /dev/null and b/.doctrees/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.1_basics.doctree b/.doctrees/tutorials/tutorials.script.core.1_basics.doctree new file mode 100644 index 0000000000..1101632019 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.1_basics.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.2_conditions.doctree b/.doctrees/tutorials/tutorials.script.core.2_conditions.doctree new file mode 100644 index 0000000000..806bdcf59c Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.2_conditions.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.3_responses.doctree b/.doctrees/tutorials/tutorials.script.core.3_responses.doctree new file mode 100644 index 0000000000..53f05e20ae Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.3_responses.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.4_transitions.doctree b/.doctrees/tutorials/tutorials.script.core.4_transitions.doctree new file mode 100644 index 0000000000..4dae60ca90 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.4_transitions.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.5_global_transitions.doctree b/.doctrees/tutorials/tutorials.script.core.5_global_transitions.doctree new file mode 100644 index 0000000000..955e0b06d2 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.5_global_transitions.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.6_context_serialization.doctree b/.doctrees/tutorials/tutorials.script.core.6_context_serialization.doctree new file mode 100644 index 0000000000..f723b626ac Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.6_context_serialization.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.7_pre_response_processing.doctree b/.doctrees/tutorials/tutorials.script.core.7_pre_response_processing.doctree new file mode 100644 index 0000000000..ccafd9acb9 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.7_pre_response_processing.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.8_misc.doctree b/.doctrees/tutorials/tutorials.script.core.8_misc.doctree new file mode 100644 index 0000000000..2e3e9666c4 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.8_misc.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.core.9_pre_transitions_processing.doctree b/.doctrees/tutorials/tutorials.script.core.9_pre_transitions_processing.doctree new file mode 100644 index 0000000000..3b1e7ca2ee Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.core.9_pre_transitions_processing.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.responses.1_basics.doctree b/.doctrees/tutorials/tutorials.script.responses.1_basics.doctree new file mode 100644 index 0000000000..dfd7363064 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.responses.1_basics.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.responses.2_buttons.doctree b/.doctrees/tutorials/tutorials.script.responses.2_buttons.doctree new file mode 100644 index 0000000000..c5f1886cb0 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.responses.2_buttons.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.responses.3_media.doctree b/.doctrees/tutorials/tutorials.script.responses.3_media.doctree new file mode 100644 index 0000000000..95e4936ee5 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.responses.3_media.doctree differ diff --git a/.doctrees/tutorials/tutorials.script.responses.4_multi_message.doctree b/.doctrees/tutorials/tutorials.script.responses.4_multi_message.doctree new file mode 100644 index 0000000000..fee1ba1942 Binary files /dev/null and b/.doctrees/tutorials/tutorials.script.responses.4_multi_message.doctree differ diff --git a/.doctrees/tutorials/tutorials.stats.1_extractor_functions.doctree b/.doctrees/tutorials/tutorials.stats.1_extractor_functions.doctree new file mode 100644 index 0000000000..c37d339491 Binary files /dev/null and b/.doctrees/tutorials/tutorials.stats.1_extractor_functions.doctree differ diff --git a/.doctrees/tutorials/tutorials.stats.2_pipeline_integration.doctree b/.doctrees/tutorials/tutorials.stats.2_pipeline_integration.doctree new file mode 100644 index 0000000000..18595ac407 Binary files /dev/null and b/.doctrees/tutorials/tutorials.stats.2_pipeline_integration.doctree differ diff --git a/.doctrees/tutorials/tutorials.utils.1_cache.doctree b/.doctrees/tutorials/tutorials.utils.1_cache.doctree new file mode 100644 index 0000000000..f9c4a98c23 Binary files /dev/null and b/.doctrees/tutorials/tutorials.utils.1_cache.doctree differ diff --git a/.doctrees/tutorials/tutorials.utils.2_lru_cache.doctree b/.doctrees/tutorials/tutorials.utils.2_lru_cache.doctree new file mode 100644 index 0000000000..6b8f487e51 Binary files /dev/null and b/.doctrees/tutorials/tutorials.utils.2_lru_cache.doctree differ diff --git a/.doctrees/user_guides.doctree b/.doctrees/user_guides.doctree new file mode 100644 index 0000000000..ba46d40d48 Binary files /dev/null and b/.doctrees/user_guides.doctree differ diff --git a/.doctrees/user_guides/basic_conceptions.doctree b/.doctrees/user_guides/basic_conceptions.doctree new file mode 100644 index 0000000000..79c3240ccd Binary files /dev/null and b/.doctrees/user_guides/basic_conceptions.doctree differ diff --git a/.doctrees/user_guides/context_guide.doctree b/.doctrees/user_guides/context_guide.doctree new file mode 100644 index 0000000000..ff86c9e006 Binary files /dev/null and b/.doctrees/user_guides/context_guide.doctree differ diff --git a/.doctrees/user_guides/optimization_guide.doctree b/.doctrees/user_guides/optimization_guide.doctree new file mode 100644 index 0000000000..a6faa09eb5 Binary files /dev/null and b/.doctrees/user_guides/optimization_guide.doctree differ diff --git a/.doctrees/user_guides/superset_guide.doctree b/.doctrees/user_guides/superset_guide.doctree new file mode 100644 index 0000000000..fd5f9dfbc3 Binary files /dev/null and b/.doctrees/user_guides/superset_guide.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/_images/annotations.png b/_images/annotations.png new file mode 100644 index 0000000000..b1e524890a Binary files /dev/null and b/_images/annotations.png differ diff --git a/_images/benchmark_compare.png b/_images/benchmark_compare.png new file mode 100644 index 0000000000..8765ed48c7 Binary files /dev/null and b/_images/benchmark_compare.png differ diff --git a/_images/benchmark_mass_compare.png b/_images/benchmark_mass_compare.png new file mode 100644 index 0000000000..71911c5546 Binary files /dev/null and b/_images/benchmark_mass_compare.png differ diff --git a/_images/benchmark_sets.png b/_images/benchmark_sets.png new file mode 100644 index 0000000000..4eadd28734 Binary files /dev/null and b/_images/benchmark_sets.png differ diff --git a/_images/benchmark_view.png b/_images/benchmark_view.png new file mode 100644 index 0000000000..c8a5afc810 Binary files /dev/null and b/_images/benchmark_view.png differ diff --git a/_images/databases.png b/_images/databases.png new file mode 100644 index 0000000000..167c4ce615 Binary files /dev/null and b/_images/databases.png differ diff --git a/_images/general_stats.png b/_images/general_stats.png new file mode 100644 index 0000000000..660f2ed89b Binary files /dev/null and b/_images/general_stats.png differ diff --git a/_images/overview.png b/_images/overview.png new file mode 100644 index 0000000000..5bd0fb6a1b Binary files /dev/null and b/_images/overview.png differ diff --git a/_images/service_stats.png b/_images/service_stats.png new file mode 100644 index 0000000000..7ce7242863 Binary files /dev/null and b/_images/service_stats.png differ diff --git a/_images/user_actor.png b/_images/user_actor.png new file mode 100644 index 0000000000..220270aafb Binary files /dev/null and b/_images/user_actor.png differ diff --git a/_misc/benchmark_schema.json b/_misc/benchmark_schema.json new file mode 100644 index 0000000000..f4ecfcf406 --- /dev/null +++ b/_misc/benchmark_schema.json @@ -0,0 +1,184 @@ +{ + "title": "Benchmark set", + "description": "A structure containing results of multiple benchmarks", + "type": "object", + "properties": { + "name": { + "description": "Name of the benchmark set", + "type": "string" + }, + "description": { + "description": "Description of the benchmark set", + "type": "string" + }, + "uuid": { + "description": "Unique id of the benchmark set", + "type": "string" + }, + "benchmarks": { + "description": "A list of benchmarks in the set with results", + "type": "array", + "items": { + "title": "Benchmark", + "description": "A singular benchmark with results", + "type": "object", + "properties": { + "name": { + "description": "Name of the benchmark", + "type": "string" + }, + "description": { + "description": "Description of the benchmark", + "type": "string" + }, + "uuid": { + "description": "Unique id of the benchmark", + "type": "string" + }, + "success": { + "description": "Whether benchmark process did not raise an exception", + "type": "boolean" + }, + "db_factory": { + "description": "Configuration of the context storage that was benchmarked", + "type": "object", + "properties": { + "uri": { + "description": "URI of the context storage", + "type": "string" + }, + "factory_module": { + "description": "A module containing context storage factory", + "type": "string" + }, + "factory": { + "description": "Name of a context storage factory inside the module", + "type": "string" + } + }, + "required": ["uri", "factory_module", "factory"] + }, + "benchmark_config": { + "description": "Configuration of the benchmark", + "type": "object" + }, + "result": { + "description": "Raw benchmark results or exception info", + "oneOf": [ + { + "type": "object", + "properties": { + "write_times": { + "description": "List of write times; list index corresponds to context_num", + "type": "array", + "items": {"type": "number", "minimum": 0} + }, + "read_times": { + "description": "List of read times w.r.t. dialog_len; list index corresponds to context_num", + "type": "array", + "items": { + "type": "object", + "description": "Dictionary in which keys are equal to dialog_len of a context and values to read time of a context" + } + }, + "update_times": { + "description": "List of update times w.r.t. dialog_len; list index corresponds to context_num", + "type": "array", + "items": { + "type": "object", + "description": "Dictionary in which keys are equal to dialog_len of a context and values to update time of a context" + } + } + }, + "required": ["write_times", "read_times", "update_times"] + }, + { + "type": "object", + "properties": { + "type": { + "description": "Class name of the exception", + "type": "string" + }, + "msg": { + "description": "Exception message", + "type": "string" + }, + "traceback": { + "description": "String representation of exception traceback", + "type": "string" + } + }, + "required": ["type", "msg", "traceback"] + } + ] + + }, + "average_results": { + "description": "Calculated average statistics for benchmark results", + "type": "object", + "properties": { + "average_write_time": {"type": "number"}, + "average_read_time": {"type": "number"}, + "average_update_time": {"type": "number"}, + "read_times_grouped_by_context_num": { + "description": "List of read times w.r.t. context_num (each number is an average read time for that context_num)", + "type": "array", + "items": {"type": "number", "minimum": 0} + }, + "read_times_grouped_by_dialog_len": { + "description": "Mapping from dialog_len to read times (each value is an average read time for that dialog_len)", + "type": "object", + "items": {"type": "number", "minimum": 0} + }, + "update_times_grouped_by_context_num": { + "description": "List of update times w.r.t. context_num (each number is an average update time for that context_num)", + "type": "array", + "items": {"type": "number", "minimum": 0} + }, + "update_times_grouped_by_dialog_len": { + "description": "Mapping from dialog_len to update times (each value is an average update time for that dialog_len)", + "type": "object", + "items": {"type": "number", "minimum": 0} + }, + "pretty_write": { + "description": "Average write time with 3 significant digits", + "type": "number", + "minimum": 0 + }, + "pretty_read": { + "description": "Average read time with 3 significant digits", + "type": "number", + "minimum": 0 + }, + "pretty_update": { + "description": "Average update time with 3 significant digits", + "type": "number", + "minimum": 0 + }, + "pretty_read+update": { + "description": "Sum of average read and update times with 3 significant digits", + "type": "number", + "minimum": 0 + } + }, + "required": [ + "average_write_time", + "average_read_time", + "average_update_time", + "read_times_grouped_by_context_num", + "read_times_grouped_by_dialog_len", + "update_times_grouped_by_context_num", + "update_times_grouped_by_dialog_len", + "pretty_write", + "pretty_read", + "pretty_update", + "pretty_read+update" + ] + } + }, + "required": ["name", "description", "uuid", "success", "db_factory", "benchmark_config", "result"] + } + } + }, + "required": ["name", "description", "uuid", "benchmarks"] +} \ No newline at end of file diff --git a/_misc/benchmark_streamlit.py b/_misc/benchmark_streamlit.py new file mode 100644 index 0000000000..f428148f19 --- /dev/null +++ b/_misc/benchmark_streamlit.py @@ -0,0 +1,403 @@ +""" +Benchmark Streamlit +------------------- +This module provides a streamlit interface to view benchmark results. +The interface has 4 tabs: + +- "Benchmark sets" -- Here you can add or remove benchmark set files (generated by `save_results_to_file` function). +- "View" -- Here you can view various stats for each benchmark in the set (as well as add benchmarks to compare tab). +- "Compare" -- Here you can compare benchmarks added via "View" tab. +- "Mass compare" -- This tab lets you compare all benchmarks in a particular set. + +Run this file with `streamlit run {path/to/this/file}`. + +When run, this module will search for a file named "benchmark_results_files.json" in the directory +where the command above was executed. +If the file does not exist there, it will be created. +The file is used to store paths to benchmark result files. + +Benchmark result files added via this module are not changed (only read). + +You can install all the dependencies of this module with +``` +pip install dff[benchmark] +``` +""" +import json +from pathlib import Path +from uuid import uuid4 + +import pandas as pd +from pympler import asizeof +from humanize import naturalsize +import altair as alt +import streamlit as st + + +st.set_page_config( + page_title="DB benchmark", + layout="wide", + initial_sidebar_state="expanded", +) + +BENCHMARK_RESULTS_FILES = Path("benchmark_results_files.json") +# This file stores links to benchmark set files generated by `save_results_to_file`. + +UPLOAD_FILES_DIR = Path("uploaded_benchmarks") +# This directory stores all the benchmarks uploaded via the streamlit interface + +UPLOAD_FILES_DIR.mkdir(exist_ok=True) + + +if not BENCHMARK_RESULTS_FILES.exists(): + with open(BENCHMARK_RESULTS_FILES, "w", encoding="utf-8") as fd: + json.dump([], fd) + +if "benchmark_files" not in st.session_state: + with open(BENCHMARK_RESULTS_FILES, "r", encoding="utf-8") as fd: + st.session_state["benchmark_files"] = json.load(fd) + +if "benchmarks" not in st.session_state: + st.session_state["benchmarks"] = {} + + for file in st.session_state["benchmark_files"]: + with open(file, "r", encoding="utf-8") as fd: + st.session_state["benchmarks"][file] = json.load(fd) + +if "compare" not in st.session_state: + st.session_state["compare"] = [] + + +def add_metrics(container, value_benchmark): + write, read, update, read_update = container.columns(4) + column_names = ("write", "read", "update", "read+update") + + if not value_benchmark["success"]: + values = {key: "-" for key in column_names} + else: + values = {key: value_benchmark["average_results"][f"pretty_{key}"] for key in column_names} + + columns = { + "write": write, + "read": read, + "update": update, + "read+update": read_update, + } + + metric_help = { + "write": "Average write time for a context with from_dialog_len turns into a clean context storage.", + "read": "Average read time (dialog_len ranges between from_dialog_len and to_dialog_len).", + "update": "Average update time (dialog_len ranges between from_dialog_len and to_dialog_len).", + "read+update": "Sum of average read and update times." + " This metric is the time context_storage interface takes during each of the dialog turns.", + } + + for column_name, column in columns.items(): + column.metric(column_name.title(), values[column_name], help=metric_help[column_name]) + + +st.sidebar.text(f"Benchmarks take {naturalsize(asizeof.asizeof(st.session_state['benchmarks']))} RAM") + +add_tab, view_tab, compare_tab, mass_compare_tab = st.tabs(["Benchmark sets", "View", "Compare", "Mass compare"]) + + +############################################################################### +# Benchmark file manipulation tab +# Allows adding and deleting benchmark files +############################################################################### + +with add_tab: + benchmark_list = [] + + for file, benchmark_set in st.session_state["benchmarks"].items(): + benchmark_list.append( + { + "file": file, + "name": benchmark_set["name"], + "description": benchmark_set["description"], + "uuid": benchmark_set["uuid"], + "delete": False, + } + ) + + benchmark_list_df = pd.DataFrame(data=benchmark_list) + + df_container = st.container() + + df_container.info("In the table below you can view all your files with benchmark results as well as delete them.") + + def edit_name_desc(): + edited_rows = st.session_state["result_df"]["edited_rows"] + + for row, edits in edited_rows.items(): + for column, column_value in edits.items(): + if column in ("name", "description"): + edited_file = benchmark_list_df.iat[row, 0] + st.session_state["benchmarks"][edited_file][column] = column_value + + with open(edited_file, "w", encoding="utf-8") as edited_fd: + json.dump(st.session_state["benchmarks"][edited_file], edited_fd) + + df_container.text(f"row {row}: changed {column} to '{column_value}'") + + edited_df = df_container.data_editor( + benchmark_list_df, + key="result_df", + disabled=("file", "uuid"), + on_change=edit_name_desc, + ) + + delete_container = st.container() + + def delete_benchmarks(): + deleted_sets = [ + f"{name} ({uuid})" for name, uuid in edited_df.loc[edited_df["delete"]][["name", "uuid"]].values + ] + + st.session_state["compare"] = [ + item for item in st.session_state["compare"] if item["benchmark_set"] not in deleted_sets + ] + + files_to_delete = edited_df.loc[edited_df["delete"]]["file"] + for file in files_to_delete: + st.session_state["benchmark_files"].remove(file) + del st.session_state["benchmarks"][file] + Path(file).unlink() + delete_container.text(f"Deleted {file}") + + with open(BENCHMARK_RESULTS_FILES, "w", encoding="utf-8") as fd: + json.dump(list(st.session_state["benchmark_files"]), fd) + + delete_container.button(label="Delete selected benchmark sets", on_click=delete_benchmarks) + + def _add_benchmark(benchmark_file, container): + benchmark_file = str(benchmark_file) + + if benchmark_file == "": + return + + if benchmark_file in st.session_state["benchmark_files"]: + container.warning(f"Benchmark file already added: {benchmark_file}") + return + + if not Path(benchmark_file).exists(): + container.warning(f"File does not exists: {benchmark_file}") + return + + with open(benchmark_file, "r", encoding="utf-8") as fd: + file_contents = json.load(fd) + + for benchmark in st.session_state["benchmarks"].values(): + if file_contents["uuid"] == benchmark["uuid"]: + container.warning(f"Benchmark with the same uuid already exists: {benchmark_file}") + return + + st.session_state["benchmark_files"].append(benchmark_file) + with open(BENCHMARK_RESULTS_FILES, "w", encoding="utf-8") as fd: + json.dump(list(st.session_state["benchmark_files"]), fd) + st.session_state["benchmarks"][benchmark_file] = file_contents + + container.text(f"Added {benchmark_file}") + + st.divider() + + st.info("Below you can upload your benchmark files.") + + upload_container = st.container() + + def process_uploaded_files(): + uploaded_files = st.session_state["benchmark_file_uploader"] + if uploaded_files is not None: + if len(uploaded_files) > 0: + new_uploaded_file_dir = UPLOAD_FILES_DIR / str(uuid4()) + new_uploaded_file_dir.mkdir() + + for file in uploaded_files: + file_path = new_uploaded_file_dir / file.name + with open(file_path, "wb") as uploaded_file_descriptor: + uploaded_file_descriptor.write(file.read()) + + _add_benchmark(file_path, upload_container) + + with upload_container.form("upload_form", clear_on_submit=True): + st.file_uploader( + "Upload benchmark results", accept_multiple_files=True, type="json", key="benchmark_file_uploader" + ) + st.form_submit_button("Submit", on_click=process_uploaded_files) + + +############################################################################### +# View tab +# Allows viewing existing benchmarks +############################################################################### + +with view_tab: + set_choice, benchmark_choice, compare = st.columns([3, 3, 1]) + + sets = { + f"{benchmark['name']} ({benchmark['uuid']})": benchmark for benchmark in st.session_state["benchmarks"].values() + } + benchmark_set = set_choice.selectbox("Benchmark set", sets.keys()) + + if benchmark_set is None: + set_choice.warning("No benchmark sets available") + st.stop() + + selected_set = sets[benchmark_set] + + set_choice.text("Set description:") + set_choice.markdown(selected_set["description"]) + + benchmarks = {f"{benchmark['name']} ({benchmark['uuid']})": benchmark for benchmark in selected_set["benchmarks"]} + + benchmark = benchmark_choice.selectbox("Benchmark", benchmarks.keys()) + + if benchmark is None: + benchmark_choice.warning("No benchmarks in the set") + st.stop() + + selected_benchmark = benchmarks[benchmark] + + benchmark_choice.text("Benchmark description:") + benchmark_choice.markdown(selected_benchmark["description"]) + + with st.expander("Benchmark stats"): + benchmark_stats = { + stat: selected_benchmark[stat] + for stat in ( + "db_factory", + "benchmark_config", + ) + } + + st.json(benchmark_stats) + + if not selected_benchmark["success"]: + exc_info = selected_benchmark["result"] + + st.warning(f"**{exc_info['type']}**: {exc_info['msg']}\n\nTraceback:\n\n```\n{exc_info['traceback']}\n```") + else: + add_metrics(st.container(), selected_benchmark) + + compare_item = { + "benchmark_set": benchmark_set, + "benchmark": benchmark, + "write": selected_benchmark["average_results"]["pretty_write"], + "read": selected_benchmark["average_results"]["pretty_read"], + "update": selected_benchmark["average_results"]["pretty_update"], + "read+update": selected_benchmark["average_results"]["pretty_read+update"], + } + + def add_results_to_compare_tab(): + if compare_item not in st.session_state["compare"]: + st.session_state["compare"].append(compare_item) + else: + st.session_state["compare"].remove(compare_item) + + item_in_compare = compare_item not in st.session_state["compare"] + + compare.button( + "Add to Compare" if item_in_compare else "Remove from Compare", + on_click=add_results_to_compare_tab, + help="Add current benchmark to the 'Compare' tab." + if item_in_compare + else "Remove current benchmark from the 'Compare' tab.", + ) + + select_graph, graph = st.columns([1, 3]) + + average_results = selected_benchmark["average_results"] + + graphs = { + "Write": selected_benchmark["result"]["write_times"], + "Read (grouped by contex_num)": average_results["read_times_grouped_by_context_num"], + "Read (grouped by dialog_len)": average_results["read_times_grouped_by_dialog_len"], + "Update (grouped by contex_num)": average_results["update_times_grouped_by_context_num"], + "Update (grouped by dialog_len)": average_results["update_times_grouped_by_dialog_len"], + } + + selected_graph = select_graph.selectbox("Select graph to display", graphs.keys()) + + graph_data = graphs[selected_graph] + + if isinstance(graph_data, dict): + data = pd.DataFrame({"dialog_len": graph_data.keys(), "time": graph_data.values()}) + else: + data = pd.DataFrame({"context_num": range(len(graph_data)), "time": graph_data}) + + chart = ( + alt.Chart(data) + .mark_circle() + .encode( + x=alt.X( + "dialog_len:Q" if isinstance(graph_data, dict) else "context_num:Q", scale=alt.Scale(zero=False) + ), + y="time:Q", + ) + .interactive() + ) + + graph.altair_chart(chart, use_container_width=True) + + +############################################################################### +# Compare tab +# Allows viewing existing benchmarks +############################################################################### + +with compare_tab: + df = pd.DataFrame(st.session_state["compare"]) + + st.info("Here you can compare metrics of different benchmarks. Add them here via the 'View' tab.") + + if not df.empty: + st.dataframe( + df.style.highlight_min( + axis=0, subset=["write", "read", "update", "read+update"], props="background-color:green;" + ).highlight_max(axis=0, subset=["write", "read", "update", "read+update"], props="background-color:red;") + ) + else: + st.warning("Currently, there are no benchmarks to compare.") + +############################################################################### +# Mass compare tab +# Allows comparing all benchmarks inside a single set +############################################################################### + +with mass_compare_tab: + st.info("Here you can compare benchmarks inside of a specific set.") + + sets = { + f"{benchmark_set['name']} ({benchmark_set['uuid']})": benchmark_set + for benchmark_set in st.session_state["benchmarks"].values() + } + benchmark_set = st.selectbox("Benchmark set", sets.keys(), key="mass_compare_selectbox") + + if benchmark_set is None: + st.warning("No benchmark sets available") + st.stop() + + selected_set = sets[benchmark_set] + + compare_items = [] + + for selected_benchmark in selected_set["benchmarks"]: + if selected_benchmark["success"]: + compare_items.append( + { + "benchmark": f"{selected_benchmark['name']} ({selected_benchmark['uuid']})", + "write": selected_benchmark["average_results"]["pretty_write"], + "read": selected_benchmark["average_results"]["pretty_read"], + "update": selected_benchmark["average_results"]["pretty_update"], + "read+update": selected_benchmark["average_results"]["pretty_read+update"], + } + ) + + df = pd.DataFrame(compare_items) + + if not df.empty: + st.dataframe( + df.style.highlight_min( + axis=0, subset=["write", "read", "update", "read+update"], props="background-color:green;" + ).highlight_max(axis=0, subset=["write", "read", "update", "read+update"], props="background-color:red;") + ) diff --git a/_modules/dff/context_storages/database.html b/_modules/dff/context_storages/database.html new file mode 100644 index 0000000000..6692eb5498 --- /dev/null +++ b/_modules/dff/context_storages/database.html @@ -0,0 +1,843 @@ + + + + + + + + + + dff.context_storages.database — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.database

+"""
+Database
+--------
+The `Database` module provides classes for managing the context storage of a dialog system.
+The module can be used to store information such as the current state of the conversation
+and other data. This module includes the intermediate class (:py:class:`.DBContextStorage`) is a class
+that developers can inherit from in order to create their own context storage solutions.
+This class implements the basic functionality and can be extended to add additional features as needed.
+"""
+import asyncio
+import importlib
+import threading
+from functools import wraps
+from abc import ABC, abstractmethod
+from typing import Callable, Hashable, Optional
+
+from .protocol import PROTOCOLS
+from ..script import Context
+
+
+
+[docs] +class DBContextStorage(ABC): + r""" + An abstract interface for `dff` DB context storages. + It includes the most essential methods of the python `dict` class. + Can not be instantiated. + + :param path: Parameter `path` should be set with the URI of the database. + It includes a prefix and the required connection credentials. + Example: postgresql+asyncpg://user:password@host:port/database + In the case of classes that save data to hard drive instead of external databases + you need to specify the location of the file, like you do in sqlite. + Keep in mind that in Windows you will have to use double backslashes '\\' + instead of forward slashes '/' when defining the file path. + + """ + + def __init__(self, path: str): + _, _, file_path = path.partition("://") + self.full_path = path + """Full path to access the context storage, as it was provided by user.""" + self.path = file_path + """`full_path` without a prefix defining db used""" + self._lock = threading.Lock() + """Threading for methods that require single thread access.""" + + def __getitem__(self, key: Hashable) -> Context: + """ + Synchronous method for accessing stored Context. + + :param key: Hashable key used to store Context instance. + :return: The stored context, associated with the given key. + """ + return asyncio.run(self.get_item_async(key)) + +
+[docs] + @abstractmethod + async def get_item_async(self, key: Hashable) -> Context: + """ + Asynchronous method for accessing stored Context. + + :param key: Hashable key used to store Context instance. + :return: The stored context, associated with the given key. + """ + raise NotImplementedError
+ + + def __setitem__(self, key: Hashable, value: Context): + """ + Synchronous method for storing Context. + + :param key: Hashable key used to store Context instance. + :param value: Context to store. + """ + return asyncio.run(self.set_item_async(key, value)) + +
+[docs] + @abstractmethod + async def set_item_async(self, key: Hashable, value: Context): + """ + Asynchronous method for storing Context. + + :param key: Hashable key used to store Context instance. + :param value: Context to store. + """ + raise NotImplementedError
+ + + def __delitem__(self, key: Hashable): + """ + Synchronous method for removing stored Context. + + :param key: Hashable key used to identify Context instance for deletion. + """ + return asyncio.run(self.del_item_async(key)) + +
+[docs] + @abstractmethod + async def del_item_async(self, key: Hashable): + """ + Asynchronous method for removing stored Context. + + :param key: Hashable key used to identify Context instance for deletion. + """ + raise NotImplementedError
+ + + def __contains__(self, key: Hashable) -> bool: + """ + Synchronous method for finding whether any Context is stored with given key. + + :param key: Hashable key used to check if Context instance is stored. + :return: True if there is Context accessible by given key, False otherwise. + """ + return asyncio.run(self.contains_async(key)) + +
+[docs] + @abstractmethod + async def contains_async(self, key: Hashable) -> bool: + """ + Asynchronous method for finding whether any Context is stored with given key. + + :param key: Hashable key used to check if Context instance is stored. + :return: True if there is Context accessible by given key, False otherwise. + """ + raise NotImplementedError
+ + + def __len__(self) -> int: + """ + Synchronous method for retrieving number of stored Contexts. + + :return: The number of stored Contexts. + """ + return asyncio.run(self.len_async()) + +
+[docs] + @abstractmethod + async def len_async(self) -> int: + """ + Asynchronous method for retrieving number of stored Contexts. + + :return: The number of stored Contexts. + """ + raise NotImplementedError
+ + +
+[docs] + def get(self, key: Hashable, default: Optional[Context] = None) -> Context: + """ + Synchronous method for accessing stored Context, returning default if no Context is stored with the given key. + + :param key: Hashable key used to store Context instance. + :param default: Optional default value to be returned if no Context is found. + :return: The stored context, associated with the given key or default value. + """ + return asyncio.run(self.get_async(key, default))
+ + +
+[docs] + async def get_async(self, key: Hashable, default: Optional[Context] = None) -> Context: + """ + Asynchronous method for accessing stored Context, returning default if no Context is stored with the given key. + + :param key: Hashable key used to store Context instance. + :param default: Optional default value to be returned if no Context is found. + :return: The stored context, associated with the given key or default value. + """ + try: + return await self.get_item_async(str(key)) + except KeyError: + return default
+ + +
+[docs] + def clear(self): + """ + Synchronous method for clearing context storage, removing all the stored Contexts. + """ + return asyncio.run(self.clear_async())
+ + +
+[docs] + @abstractmethod + async def clear_async(self): + """ + Asynchronous method for clearing context storage, removing all the stored Contexts. + """ + raise NotImplementedError
+
+ + + +
+[docs] +def threadsafe_method(func: Callable): + """ + A decorator that makes sure methods of an object instance are threadsafe. + """ + + @wraps(func) + def _synchronized(self, *args, **kwargs): + with self._lock: + return func(self, *args, **kwargs) + + return _synchronized
+ + + +
+[docs] +def context_storage_factory(path: str, **kwargs) -> DBContextStorage: + """ + Use context_storage_factory to lazy import context storage types and instantiate them. + The function takes a database connection URI or its equivalent. It should be prefixed with database name, + followed by the symbol triplet '://'. + + Then, you should list the connection parameters like this: user:password@host:port/database + The whole URI will then look like this: + + - shelve://path_to_the_file/file_name + - json://path_to_the_file/file_name + - pickle://path_to_the_file/file_name + - sqlite+aiosqlite://path_to_the_file/file_name + - redis://:pass@localhost:6378/0 + - mongodb://admin:pass@localhost:27016/admin + - mysql+asyncmy://root:pass@localhost:3306/test + - postgresql+asyncpg://postgres:pass@localhost:5430/test + - grpc://localhost:2134/local + - grpcs://localhost:2134/local + + For context storages that write to local files, the function expects a file path instead of connection params: + json://file.json + When using sqlite backend your prefix should contain three slashes if you use Windows, or four in other cases: + sqlite:////file.db + If you want to use additional parameters in class constructors, you can pass them to this function as kwargs. + + :param path: Path to the file. + """ + prefix, _, _ = path.partition("://") + if "sql" in prefix: + prefix = prefix.split("+")[0] # this takes care of alternative sql drivers + assert ( + prefix in PROTOCOLS + ), f""" + URI path should be prefixed with one of the following:\n + {", ".join(PROTOCOLS.keys())}.\n + For more information, see the function doc:\n{context_storage_factory.__doc__} + """ + _class, module = PROTOCOLS[prefix]["class"], PROTOCOLS[prefix]["module"] + target_class = getattr(importlib.import_module(f".{module}", package="dff.context_storages"), _class) + return target_class(path, **kwargs)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/json.html b/_modules/dff/context_storages/json.html new file mode 100644 index 0000000000..90a8d74ada --- /dev/null +++ b/_modules/dff/context_storages/json.html @@ -0,0 +1,697 @@ + + + + + + + + + + dff.context_storages.json — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.json

+"""
+JSON
+----
+The JSON module provides a json-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data in a JSON. It allows the DFF to easily
+store and retrieve context data.
+"""
+import asyncio
+from typing import Hashable
+
+try:
+    import aiofiles
+    import aiofiles.os
+
+    json_available = True
+except ImportError:
+    json_available = False
+
+from pydantic import BaseModel, model_validator
+
+from .database import DBContextStorage, threadsafe_method
+from dff.script import Context
+
+
+
+[docs] +class SerializableStorage(BaseModel, extra="allow"): +
+[docs] + @model_validator(mode="before") + @classmethod + def validate_any(cls, vals): + for key, value in vals.items(): + vals[key] = Context.cast(value) + return vals
+
+ + + +
+[docs] +class JSONContextStorage(DBContextStorage): + """ + Implements :py:class:`.DBContextStorage` with `json` as the storage format. + + :param path: Target file URI. Example: `json://file.json`. + """ + + def __init__(self, path: str): + DBContextStorage.__init__(self, path) + asyncio.run(self._load()) + +
+[docs] + @threadsafe_method + async def len_async(self) -> int: + return len(self.storage.model_extra)
+ + +
+[docs] + @threadsafe_method + async def set_item_async(self, key: Hashable, value: Context): + self.storage.model_extra.__setitem__(str(key), value) + await self._save()
+ + +
+[docs] + @threadsafe_method + async def get_item_async(self, key: Hashable) -> Context: + await self._load() + return Context.cast(self.storage.model_extra.__getitem__(str(key)))
+ + +
+[docs] + @threadsafe_method + async def del_item_async(self, key: Hashable): + self.storage.model_extra.__delitem__(str(key)) + await self._save()
+ + +
+[docs] + @threadsafe_method + async def contains_async(self, key: Hashable) -> bool: + await self._load() + return self.storage.model_extra.__contains__(str(key))
+ + +
+[docs] + @threadsafe_method + async def clear_async(self): + self.storage.model_extra.clear() + await self._save()
+ + +
+[docs] + async def _save(self): + async with aiofiles.open(self.path, "w+", encoding="utf-8") as file_stream: + await file_stream.write(self.storage.model_dump_json())
+ + +
+[docs] + async def _load(self): + if not await aiofiles.os.path.isfile(self.path) or (await aiofiles.os.stat(self.path)).st_size == 0: + self.storage = SerializableStorage() + await self._save() + else: + async with aiofiles.open(self.path, "r", encoding="utf-8") as file_stream: + self.storage = SerializableStorage.model_validate_json(await file_stream.read())
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/mongo.html b/_modules/dff/context_storages/mongo.html new file mode 100644 index 0000000000..e1cfe2500d --- /dev/null +++ b/_modules/dff/context_storages/mongo.html @@ -0,0 +1,698 @@ + + + + + + + + + + dff.context_storages.mongo — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.mongo

+"""
+Mongo
+-----
+The Mongo module provides a MongoDB-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data in a MongoDB.
+It allows the DFF to easily store and retrieve context data in a format that is highly scalable
+and easy to work with.
+
+MongoDB is a widely-used, open-source NoSQL database that is known for its scalability and performance.
+It stores data in a format similar to JSON, making it easy to work with the data in a variety of programming languages
+and environments. Additionally, MongoDB is highly scalable and can handle large amounts of data
+and high levels of read and write traffic.
+"""
+from typing import Hashable, Dict, Any
+
+try:
+    from motor.motor_asyncio import AsyncIOMotorClient
+    from bson.objectid import ObjectId
+
+    mongo_available = True
+except ImportError:
+    mongo_available = False
+    AsyncIOMotorClient = None
+    ObjectId = Any
+
+import json
+
+from dff.script import Context
+
+from .database import DBContextStorage, threadsafe_method
+from .protocol import get_protocol_install_suggestion
+
+
+
+[docs] +class MongoContextStorage(DBContextStorage): + """ + Implements :py:class:`.DBContextStorage` with `mongodb` as the database backend. + + :param path: Database URI. Example: `mongodb://user:password@host:port/dbname`. + :param collection: Name of the collection to store the data in. + """ + + def __init__(self, path: str, collection: str = "context_collection"): + DBContextStorage.__init__(self, path) + if not mongo_available: + install_suggestion = get_protocol_install_suggestion("mongodb") + raise ImportError("`mongodb` package is missing.\n" + install_suggestion) + self._mongo = AsyncIOMotorClient(self.full_path) + db = self._mongo.get_default_database() + self.collection = db[collection] + +
+[docs] + @staticmethod + def _adjust_key(key: Hashable) -> Dict[str, ObjectId]: + """Convert a n-digit context id to a 24-digit mongo id""" + new_key = hex(int.from_bytes(str.encode(str(key)), "big", signed=False))[3:] + new_key = (new_key * (24 // len(new_key) + 1))[:24] + assert len(new_key) == 24 + return {"_id": ObjectId(new_key)}
+ + +
+[docs] + @threadsafe_method + async def set_item_async(self, key: Hashable, value: Context): + new_key = self._adjust_key(key) + value = value if isinstance(value, Context) else Context.cast(value) + document = json.loads(value.model_dump_json()) + + document.update(new_key) + await self.collection.replace_one(new_key, document, upsert=True)
+ + +
+[docs] + @threadsafe_method + async def get_item_async(self, key: Hashable) -> Context: + adjust_key = self._adjust_key(key) + document = await self.collection.find_one(adjust_key) + if document: + document.pop("_id") + ctx = Context.cast(document) + return ctx + raise KeyError
+ + +
+[docs] + @threadsafe_method + async def del_item_async(self, key: Hashable): + adjust_key = self._adjust_key(key) + await self.collection.delete_one(adjust_key)
+ + +
+[docs] + @threadsafe_method + async def contains_async(self, key: Hashable) -> bool: + adjust_key = self._adjust_key(key) + return bool(await self.collection.find_one(adjust_key))
+ + +
+[docs] + @threadsafe_method + async def len_async(self) -> int: + return await self.collection.estimated_document_count()
+ + +
+[docs] + @threadsafe_method + async def clear_async(self): + await self.collection.delete_many(dict())
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/pickle.html b/_modules/dff/context_storages/pickle.html new file mode 100644 index 0000000000..0d3fa8532c --- /dev/null +++ b/_modules/dff/context_storages/pickle.html @@ -0,0 +1,686 @@ + + + + + + + + + + dff.context_storages.pickle — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.pickle

+"""
+Pickle
+------
+The Pickle module provides a pickle-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data in a pickle format.
+It allows the DFF to easily store and retrieve context data in a format that is efficient
+for serialization and deserialization and can be easily used in python.
+
+Pickle is a python library that allows to serialize and deserialize python objects.
+It is efficient and fast, but it is not recommended to use it to transfer data across
+different languages or platforms because it's not cross-language compatible.
+"""
+import asyncio
+import pickle
+from typing import Hashable
+
+try:
+    import aiofiles
+    import aiofiles.os
+
+    pickle_available = True
+except ImportError:
+    pickle_available = False
+
+from .database import DBContextStorage, threadsafe_method
+from dff.script import Context
+
+
+
+[docs] +class PickleContextStorage(DBContextStorage): + """ + Implements :py:class:`.DBContextStorage` with `pickle` as driver. + + :param path: Target file URI. Example: 'pickle://file.pkl'. + """ + + def __init__(self, path: str): + DBContextStorage.__init__(self, path) + asyncio.run(self._load()) + +
+[docs] + @threadsafe_method + async def len_async(self) -> int: + return len(self.dict)
+ + +
+[docs] + @threadsafe_method + async def set_item_async(self, key: Hashable, value: Context): + self.dict.__setitem__(str(key), value) + await self._save()
+ + +
+[docs] + @threadsafe_method + async def get_item_async(self, key: Hashable) -> Context: + await self._load() + return Context.cast(self.dict.__getitem__(str(key)))
+ + +
+[docs] + @threadsafe_method + async def del_item_async(self, key: Hashable): + self.dict.__delitem__(str(key)) + await self._save()
+ + +
+[docs] + @threadsafe_method + async def contains_async(self, key: Hashable) -> bool: + await self._load() + return self.dict.__contains__(str(key))
+ + +
+[docs] + @threadsafe_method + async def clear_async(self): + self.dict.clear() + await self._save()
+ + +
+[docs] + async def _save(self): + async with aiofiles.open(self.path, "wb+") as file: + await file.write(pickle.dumps(self.dict))
+ + +
+[docs] + async def _load(self): + if not await aiofiles.os.path.isfile(self.path) or (await aiofiles.os.stat(self.path)).st_size == 0: + self.dict = dict() + await self._save() + else: + async with aiofiles.open(self.path, "rb") as file: + self.dict = pickle.loads(await file.read())
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/protocol.html b/_modules/dff/context_storages/protocol.html new file mode 100644 index 0000000000..bd81a5fc86 --- /dev/null +++ b/_modules/dff/context_storages/protocol.html @@ -0,0 +1,617 @@ + + + + + + + + + + dff.context_storages.protocol — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.protocol

+"""
+Protocol
+--------
+The Protocol module contains the base code for the different communication protocols used in the DFF.
+It defines the :py:data:`.PROTOCOLS` constant, which lists all the supported protocols in the DFF.
+
+The module also includes a function :py:func:`.get_protocol_install_suggestion()` that is used to provide
+suggestions for installing the necessary dependencies for a specific protocol.
+This function takes the name of the desired protocol as an argument and returns
+a string containing the necessary installation commands for that protocol.
+
+The DFF supports a variety of communication protocols,
+which allows it to communicate with different types of databases.
+"""
+import json
+import pathlib
+
+with open(pathlib.Path(__file__).parent / "protocols.json", "r", encoding="utf-8") as protocols:
+    PROTOCOLS = json.load(protocols)
+_prtocol_keys = {"module", "class", "slug", "uri_example"}
+assert all(set(proc.keys()) == _prtocol_keys for proc in PROTOCOLS.values()), "Protocols are incomplete"
+
+
+
+[docs] +def get_protocol_install_suggestion(protocol_name: str) -> str: + """ + Provide suggestions for installing the necessary dependencies for a specific protocol. + + :param protocol_name: Protocol name. + """ + protocol = PROTOCOLS.get(protocol_name, {}) + slug = protocol.get("slug") + if slug: + return f"Try to run `pip install dff[{slug}]`" + return ""
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/redis.html b/_modules/dff/context_storages/redis.html new file mode 100644 index 0000000000..5052145a73 --- /dev/null +++ b/_modules/dff/context_storages/redis.html @@ -0,0 +1,673 @@ + + + + + + + + + + dff.context_storages.redis — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.redis

+"""
+Redis
+-----
+The Redis module provides a Redis-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data in a Redis.
+It allows the DFF to easily store and retrieve context data in a format that is highly scalable
+and easy to work with.
+
+Redis is an open-source, in-memory data structure store that is known for its
+high performance and scalability. It stores data in key-value pairs and supports a variety of data
+structures such as strings, hashes, lists, sets, and more.
+Additionally, Redis can be used as a cache, message broker, and database, making it a versatile
+and powerful choice for data storage and management.
+"""
+import json
+from typing import Hashable
+
+try:
+    from redis.asyncio import Redis
+
+    redis_available = True
+except ImportError:
+    redis_available = False
+
+from dff.script import Context
+
+from .database import DBContextStorage, threadsafe_method
+from .protocol import get_protocol_install_suggestion
+
+
+
+[docs] +class RedisContextStorage(DBContextStorage): + """ + Implements :py:class:`.DBContextStorage` with `redis` as the database backend. + + :param path: Database URI string. Example: `redis://user:password@host:port`. + """ + + def __init__(self, path: str): + DBContextStorage.__init__(self, path) + if not redis_available: + install_suggestion = get_protocol_install_suggestion("redis") + raise ImportError("`redis` package is missing.\n" + install_suggestion) + self._redis = Redis.from_url(self.full_path) + +
+[docs] + @threadsafe_method + async def contains_async(self, key: Hashable) -> bool: + return bool(await self._redis.exists(str(key)))
+ + +
+[docs] + @threadsafe_method + async def set_item_async(self, key: Hashable, value: Context): + value = value if isinstance(value, Context) else Context.cast(value) + await self._redis.set(str(key), value.model_dump_json())
+ + +
+[docs] + @threadsafe_method + async def get_item_async(self, key: Hashable) -> Context: + result = await self._redis.get(str(key)) + if result: + result_dict = json.loads(result.decode("utf-8")) + return Context.cast(result_dict) + raise KeyError(f"No entry for key {key}.")
+ + +
+[docs] + @threadsafe_method + async def del_item_async(self, key: Hashable): + await self._redis.delete(str(key))
+ + +
+[docs] + @threadsafe_method + async def len_async(self) -> int: + return await self._redis.dbsize()
+ + +
+[docs] + @threadsafe_method + async def clear_async(self): + await self._redis.flushdb()
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/shelve.html b/_modules/dff/context_storages/shelve.html new file mode 100644 index 0000000000..5da0312901 --- /dev/null +++ b/_modules/dff/context_storages/shelve.html @@ -0,0 +1,652 @@ + + + + + + + + + + dff.context_storages.shelve — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.shelve

+"""
+Shelve
+------
+The Shelve module provides a shelve-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data in a shelve format.
+It allows the DFF to easily store and retrieve context data in a format that is efficient
+for serialization and deserialization and can be easily used in python.
+
+Shelve is a python library that allows to store and retrieve python objects.
+It is efficient and fast, but it is not recommended to use it to transfer data across different languages
+or platforms because it's not cross-language compatible.
+It stores data in a dbm-style format in the file system, which is not as fast as the other serialization
+libraries like pickle or JSON.
+"""
+import pickle
+from shelve import DbfilenameShelf
+from typing import Hashable
+
+from dff.script import Context
+
+from .database import DBContextStorage
+
+
+
+[docs] +class ShelveContextStorage(DBContextStorage): + """ + Implements :py:class:`.DBContextStorage` with `shelve` as the driver. + + :param path: Target file URI. Example: `shelve://file.db`. + """ + + def __init__(self, path: str): + DBContextStorage.__init__(self, path) + self.shelve_db = DbfilenameShelf(filename=self.path, protocol=pickle.HIGHEST_PROTOCOL) + +
+[docs] + async def get_item_async(self, key: Hashable) -> Context: + return self.shelve_db[str(key)]
+ + +
+[docs] + async def set_item_async(self, key: Hashable, value: Context): + self.shelve_db.__setitem__(str(key), value)
+ + +
+[docs] + async def del_item_async(self, key: Hashable): + self.shelve_db.__delitem__(str(key))
+ + +
+[docs] + async def contains_async(self, key: Hashable) -> bool: + return self.shelve_db.__contains__(str(key))
+ + +
+[docs] + async def len_async(self) -> int: + return self.shelve_db.__len__()
+ + +
+[docs] + async def clear_async(self): + self.shelve_db.clear()
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/sql.html b/_modules/dff/context_storages/sql.html new file mode 100644 index 0000000000..4393506d6d --- /dev/null +++ b/_modules/dff/context_storages/sql.html @@ -0,0 +1,804 @@ + + + + + + + + + + dff.context_storages.sql — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.sql

+"""
+SQL
+---
+The SQL module provides a SQL-based version of the :py:class:`.DBContextStorage` class.
+This class is used to store and retrieve context data from SQL databases.
+It allows the DFF to easily store and retrieve context data in a format that is highly scalable
+and easy to work with.
+
+The SQL module provides the ability to choose the backend of your choice from
+MySQL, PostgreSQL, or SQLite. You can choose the one that is most suitable for your use case and environment.
+MySQL and PostgreSQL are widely used open-source relational databases that are known for their
+reliability and scalability. SQLite is a self-contained, high-reliability, embedded, full-featured,
+public-domain, SQL database engine.
+"""
+import asyncio
+import importlib
+import json
+from typing import Hashable
+
+from dff.script import Context
+
+from .database import DBContextStorage, threadsafe_method
+from .protocol import get_protocol_install_suggestion
+
+try:
+    from sqlalchemy import Table, MetaData, Column, JSON, String, inspect, select, delete, func
+    from sqlalchemy.ext.asyncio import create_async_engine
+
+    sqlalchemy_available = True
+except (ImportError, ModuleNotFoundError):
+    sqlalchemy_available = False
+
+postgres_available = sqlite_available = mysql_available = False
+
+try:
+    import asyncpg
+
+    _ = asyncpg
+
+    postgres_available = True
+except (ImportError, ModuleNotFoundError):
+    pass
+
+try:
+    import asyncmy
+
+    _ = asyncmy
+
+    mysql_available = True
+except (ImportError, ModuleNotFoundError):
+    pass
+
+try:
+    import aiosqlite
+
+    _ = aiosqlite
+
+    sqlite_available = True
+except (ImportError, ModuleNotFoundError):
+    pass
+
+if not sqlalchemy_available:
+    postgres_available = sqlite_available = mysql_available = False
+
+
+
+[docs] +def import_insert_for_dialect(dialect: str): + """ + Imports the insert function into global scope depending on the chosen sqlalchemy dialect. + + :param dialect: Chosen sqlalchemy dialect. + """ + global insert + insert = getattr( + importlib.import_module(f"sqlalchemy.dialects.{dialect}"), + "insert", + )
+ + + +
+[docs] +class SQLContextStorage(DBContextStorage): + """ + | SQL-based version of the :py:class:`.DBContextStorage`. + | Compatible with MySQL, Postgresql, Sqlite. + + :param path: Standard sqlalchemy URI string. + When using sqlite backend in Windows, keep in mind that you have to use double backslashes '\\' + instead of forward slashes '/' in the file path. + :param table_name: The name of the table to use. + :param custom_driver: If you intend to use some other database driver instead of the recommended ones, + set this parameter to `True` to bypass the import checks. + """ + + def __init__(self, path: str, table_name: str = "contexts", custom_driver: bool = False): + DBContextStorage.__init__(self, path) + + self._check_availability(custom_driver) + self.engine = create_async_engine(self.full_path) + self.dialect: str = self.engine.dialect.name + + id_column_args = {"primary_key": True} + if self.dialect == "sqlite": + id_column_args["sqlite_on_conflict_primary_key"] = "REPLACE" + + self.metadata = MetaData() + self.table = Table( + table_name, + self.metadata, + Column("id", String(36), **id_column_args), + Column("context", JSON), # column for storing serialized contexts + ) + + asyncio.run(self._create_self_table()) + + import_insert_for_dialect(self.dialect) + +
+[docs] + @threadsafe_method + async def set_item_async(self, key: Hashable, value: Context): + value = value if isinstance(value, Context) else Context.cast(value) + value = json.loads(value.model_dump_json()) + + insert_stmt = insert(self.table).values(id=str(key), context=value) + update_stmt = await self._get_update_stmt(insert_stmt) + + async with self.engine.connect() as conn: + await conn.execute(update_stmt) + await conn.commit()
+ + +
+[docs] + @threadsafe_method + async def get_item_async(self, key: Hashable) -> Context: + stmt = select(self.table.c.context).where(self.table.c.id == str(key)) + async with self.engine.connect() as conn: + result = await conn.execute(stmt) + row = result.fetchone() + if row: + return Context.cast(row[0]) + raise KeyError
+ + +
+[docs] + @threadsafe_method + async def del_item_async(self, key: Hashable): + stmt = delete(self.table).where(self.table.c.id == str(key)) + async with self.engine.connect() as conn: + await conn.execute(stmt) + await conn.commit()
+ + +
+[docs] + @threadsafe_method + async def contains_async(self, key: Hashable) -> bool: + stmt = select(self.table.c.context).where(self.table.c.id == str(key)) + async with self.engine.connect() as conn: + result = await conn.execute(stmt) + return bool(result.fetchone())
+ + +
+[docs] + @threadsafe_method + async def len_async(self) -> int: + stmt = select(func.count()).select_from(self.table) + async with self.engine.connect() as conn: + result = await conn.execute(stmt) + return result.fetchone()[0]
+ + +
+[docs] + @threadsafe_method + async def clear_async(self): + stmt = delete(self.table) + async with self.engine.connect() as conn: + await conn.execute(stmt) + await conn.commit()
+ + +
+[docs] + async def _create_self_table(self): + async with self.engine.begin() as conn: + if not await conn.run_sync(lambda sync_conn: inspect(sync_conn).has_table(self.table.name)): + await conn.run_sync(self.table.create, self.engine)
+ + +
+[docs] + async def _get_update_stmt(self, insert_stmt): + if self.dialect == "sqlite": + return insert_stmt + elif self.dialect == "mysql": + update_stmt = insert_stmt.on_duplicate_key_update(context=insert_stmt.inserted.context) + else: + update_stmt = insert_stmt.on_conflict_do_update( + index_elements=["id"], set_=dict(context=insert_stmt.excluded.context) + ) + return update_stmt
+ + +
+[docs] + def _check_availability(self, custom_driver: bool): + if not custom_driver: + if self.full_path.startswith("postgresql") and not postgres_available: + install_suggestion = get_protocol_install_suggestion("postgresql") + raise ImportError("Packages `sqlalchemy` and/or `asyncpg` are missing.\n" + install_suggestion) + elif self.full_path.startswith("mysql") and not mysql_available: + install_suggestion = get_protocol_install_suggestion("mysql") + raise ImportError("Packages `sqlalchemy` and/or `asyncmy` are missing.\n" + install_suggestion) + elif self.full_path.startswith("sqlite") and not sqlite_available: + install_suggestion = get_protocol_install_suggestion("sqlite") + raise ImportError("Package `sqlalchemy` and/or `aiosqlite` is missing.\n" + install_suggestion)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/context_storages/ydb.html b/_modules/dff/context_storages/ydb.html new file mode 100644 index 0000000000..23ae0bde44 --- /dev/null +++ b/_modules/dff/context_storages/ydb.html @@ -0,0 +1,849 @@ + + + + + + + + + + dff.context_storages.ydb — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.context_storages.ydb

+"""
+Yandex DB
+---------
+The Yandex DB module provides a version of the :py:class:`.DBContextStorage` class that designed to work with
+Yandex and other databases. Yandex DataBase is a fully-managed cloud-native SQL service that makes it easy to set up,
+operate, and scale high-performance and high-availability databases for your applications.
+
+The Yandex DB module uses the Yandex Cloud SDK, which is a python library that allows you to work
+with Yandex Cloud services using python. This allows the DFF to easily integrate with the Yandex DataBase and
+take advantage of the scalability and high-availability features provided by the service.
+"""
+import asyncio
+import os
+from typing import Hashable
+from urllib.parse import urlsplit
+
+
+from dff.script import Context
+
+from .database import DBContextStorage
+from .protocol import get_protocol_install_suggestion
+
+try:
+    import ydb
+    import ydb.aio
+
+    ydb_available = True
+except ImportError:
+    ydb_available = False
+
+
+
+[docs] +class YDBContextStorage(DBContextStorage): + """ + Version of the :py:class:`.DBContextStorage` for YDB. + + :param path: Standard sqlalchemy URI string. + When using sqlite backend in Windows, keep in mind that you have to use double backslashes '\\' + instead of forward slashes '/' in the file path. + :param table_name: The name of the table to use. + """ + + def __init__(self, path: str, table_name: str = "contexts", timeout=5): + DBContextStorage.__init__(self, path) + protocol, netloc, self.database, _, _ = urlsplit(path) + self.endpoint = "{}://{}".format(protocol, netloc) + self.table_name = table_name + if not ydb_available: + install_suggestion = get_protocol_install_suggestion("grpc") + raise ImportError("`ydb` package is missing.\n" + install_suggestion) + self.driver, self.pool = asyncio.run(_init_drive(timeout, self.endpoint, self.database, self.table_name)) + +
+[docs] + async def set_item_async(self, key: Hashable, value: Context): + value = value if isinstance(value, Context) else Context.cast(value) + + async def callee(session): + query = """ + PRAGMA TablePathPrefix("{}"); + DECLARE $queryId AS Utf8; + DECLARE $queryContext AS Json; + UPSERT INTO {} + ( + id, + context + ) + VALUES + ( + $queryId, + $queryContext + ); + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + {"$queryId": str(key), "$queryContext": value.model_dump_json()}, + commit_tx=True, + ) + + return await self.pool.retry_operation(callee)
+ + +
+[docs] + async def get_item_async(self, key: Hashable) -> Context: + async def callee(session): + query = """ + PRAGMA TablePathPrefix("{}"); + DECLARE $queryId AS Utf8; + SELECT + id, + context + FROM {} + WHERE id = $queryId; + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + result_sets = await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + { + "$queryId": str(key), + }, + commit_tx=True, + ) + if result_sets[0].rows: + return Context.cast(result_sets[0].rows[0].context) + else: + raise KeyError + + return await self.pool.retry_operation(callee)
+ + +
+[docs] + async def del_item_async(self, key: Hashable): + async def callee(session): + query = """ + PRAGMA TablePathPrefix("{}"); + DECLARE $queryId AS Utf8; + DELETE + FROM {} + WHERE + id = $queryId + ; + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + {"$queryId": str(key)}, + commit_tx=True, + ) + + return await self.pool.retry_operation(callee)
+ + +
+[docs] + async def contains_async(self, key: Hashable) -> bool: + async def callee(session): + # new transaction in serializable read write mode + # if query successfully completed you will get result sets. + # otherwise exception will be raised + query = """ + PRAGMA TablePathPrefix("{}"); + DECLARE $queryId AS Utf8; + SELECT + id, + context + FROM {} + WHERE id = $queryId; + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + result_sets = await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + { + "$queryId": str(key), + }, + commit_tx=True, + ) + return len(result_sets[0].rows) > 0 + + return await self.pool.retry_operation(callee)
+ + +
+[docs] + async def len_async(self) -> int: + async def callee(session): + query = """ + PRAGMA TablePathPrefix("{}"); + SELECT + COUNT(*) as cnt + FROM {} + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + result_sets = await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + commit_tx=True, + ) + return result_sets[0].rows[0].cnt + + return await self.pool.retry_operation(callee)
+ + +
+[docs] + async def clear_async(self): + async def callee(session): + query = """ + PRAGMA TablePathPrefix("{}"); + DECLARE $queryId AS Utf8; + DELETE + FROM {} + ; + """.format( + self.database, self.table_name + ) + prepared_query = await session.prepare(query) + + await session.transaction(ydb.SerializableReadWrite()).execute( + prepared_query, + {}, + commit_tx=True, + ) + + return await self.pool.retry_operation(callee)
+
+ + + +
+[docs] +async def _init_drive(timeout: int, endpoint: str, database: str, table_name: str): + driver = ydb.aio.Driver(endpoint=endpoint, database=database) + await driver.wait(fail_fast=True, timeout=timeout) + + pool = ydb.aio.SessionPool(driver, size=10) + + if not await _is_table_exists(pool, database, table_name): # create table if it does not exist + await _create_table(pool, database, table_name) + return driver, pool
+ + + +
+[docs] +async def _is_table_exists(pool, path, table_name) -> bool: + try: + + async def callee(session): + await session.describe_table(os.path.join(path, table_name)) + + await pool.retry_operation(callee) + return True + except ydb.SchemeError: + return False
+ + + +
+[docs] +async def _create_table(pool, path, table_name): + async def callee(session): + await session.create_table( + "/".join([path, table_name]), + ydb.TableDescription() + .with_column(ydb.Column("id", ydb.OptionalType(ydb.PrimitiveType.Utf8))) + .with_column(ydb.Column("context", ydb.OptionalType(ydb.PrimitiveType.Json))) + .with_primary_key("id"), + ) + + return await pool.retry_operation(callee)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/messengers/common/interface.html b/_modules/dff/messengers/common/interface.html new file mode 100644 index 0000000000..c649ceea6f --- /dev/null +++ b/_modules/dff/messengers/common/interface.html @@ -0,0 +1,822 @@ + + + + + + + + + + dff.messengers.common.interface — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.messengers.common.interface

+"""
+Message Interfaces
+------------------
+The Message Interfaces module contains several basic classes that define the message interfaces.
+These classes provide a way to define the structure of the messengers that are used to communicate with the DFF.
+"""
+import abc
+import asyncio
+import logging
+import uuid
+from typing import Optional, Any, List, Tuple, TextIO, Hashable
+
+from dff.script import Context, Message
+
+from .types import PipelineRunnerFunction, PollingInterfaceLoopFunction
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class MessengerInterface(abc.ABC): + """ + Class that represents a message interface used for communication between pipeline and users. + It is responsible for connection between user and pipeline, as well as for request-response transactions. + """ + +
+[docs] + @abc.abstractmethod + async def connect(self, pipeline_runner: PipelineRunnerFunction): + """ + Method invoked when message interface is instantiated and connection is established. + May be used for sending an introduction message or displaying general bot information. + + :param pipeline_runner: A function that should return pipeline response to user request; + usually it's a :py:meth:`~dff.pipeline.pipeline.pipeline.Pipeline._run_pipeline` function. + :type pipeline_runner: PipelineRunnerFunction + """ + raise NotImplementedError
+
+ + + +
+[docs] +class PollingMessengerInterface(MessengerInterface): + """ + Polling message interface runs in a loop, constantly asking users for a new input. + """ + +
+[docs] + @abc.abstractmethod + def _request(self) -> List[Tuple[Message, Hashable]]: + """ + Method used for sending users request for their input. + + :return: A list of tuples: user inputs and context ids (any user ids) associated with the inputs. + """ + raise NotImplementedError
+ + +
+[docs] + @abc.abstractmethod + def _respond(self, responses: List[Context]): + """ + Method used for sending users responses for their last input. + + :param responses: A list of contexts, representing dialogs with the users; + `last_response`, `id` and some dialog info can be extracted from there. + """ + raise NotImplementedError
+ + +
+[docs] + def _on_exception(self, e: BaseException): + """ + Method that is called on polling cycle exceptions, in some cases it should show users the exception. + By default, it logs all exit exceptions to `info` log and all non-exit exceptions to `error`. + + :param e: The exception. + """ + if isinstance(e, Exception): + logger.error(f"Exception in {type(self).__name__} loop!\n{str(e)}") + else: + logger.info(f"{type(self).__name__} has stopped polling.")
+ + +
+[docs] + async def _polling_loop( + self, + pipeline_runner: PipelineRunnerFunction, + timeout: float = 0, + ): + """ + Method running the request - response cycle once. + """ + user_updates = self._request() + responses = [await pipeline_runner(request, ctx_id) for request, ctx_id in user_updates] + self._respond(responses) + await asyncio.sleep(timeout)
+ + +
+[docs] + async def connect( + self, + pipeline_runner: PipelineRunnerFunction, + loop: PollingInterfaceLoopFunction = lambda: True, + timeout: float = 0, + ): + """ + Method, running a request - response cycle in a loop. + The looping behavior is determined by `loop` and `timeout`, + for most cases the loop itself shouldn't be overridden. + + :param pipeline_runner: A function that should return pipeline response to user request; + usually it's a :py:meth:`~dff.pipeline.pipeline.pipeline.Pipeline._run_pipeline` function. + :type pipeline_runner: PipelineRunnerFunction + :param loop: a function that determines whether polling should be continued; + called in each cycle, should return `True` to continue polling or `False` to stop. + :type loop: PollingInterfaceLoopFunction + :param timeout: a time interval between polls (in seconds). + """ + while loop(): + try: + await self._polling_loop(pipeline_runner, timeout) + + except BaseException as e: + self._on_exception(e) + break
+
+ + + +
+[docs] +class CallbackMessengerInterface(MessengerInterface): + """ + Callback message interface is waiting for user input and answers once it gets one. + """ + + def __init__(self): + self._pipeline_runner: Optional[PipelineRunnerFunction] = None + +
+[docs] + async def connect(self, pipeline_runner: PipelineRunnerFunction): + self._pipeline_runner = pipeline_runner
+ + +
+[docs] + async def on_request_async(self, request: Any, ctx_id: Hashable) -> Context: + """ + Method invoked on user input. This method works just like + :py:meth:`~dff.pipeline.pipeline.pipeline.Pipeline._run_pipeline`, + however callback message interface may contain additional functionality (e.g. for external API accessing). + Return context that represents dialog with the user; + `last_response`, `id` and some dialog info can be extracted from there. + + :param request: User input. + :param ctx_id: Any unique id that will be associated with dialog between this user and pipeline. + :return: Context that represents dialog with the user. + """ + return await self._pipeline_runner(request, ctx_id)
+ + +
+[docs] + def on_request(self, request: Any, ctx_id: Hashable) -> Context: + """ + Method invoked on user input. This method works just like + :py:meth:`~dff.pipeline.pipeline.pipeline.Pipeline._run_pipeline`, + however callback message interface may contain additional functionality (e.g. for external API accessing). + Return context that represents dialog with the user; + `last_response`, `id` and some dialog info can be extracted from there. + + :param request: User input. + :param ctx_id: Any unique id that will be associated with dialog between this user and pipeline. + :return: Context that represents dialog with the user. + """ + return asyncio.run(self.on_request_async(request, ctx_id))
+
+ + + +
+[docs] +class CLIMessengerInterface(PollingMessengerInterface): + """ + Command line message interface is the default message interface, communicating with user via `STDIN/STDOUT`. + This message interface can maintain dialog with one user at a time only. + """ + + def __init__( + self, + intro: Optional[str] = None, + prompt_request: str = "request: ", + prompt_response: str = "response: ", + out_descriptor: Optional[TextIO] = None, + ): + super().__init__() + self._ctx_id: Optional[Hashable] = None + self._intro: Optional[str] = intro + self._prompt_request: str = prompt_request + self._prompt_response: str = prompt_response + self._descriptor: Optional[TextIO] = out_descriptor + +
+[docs] + def _request(self) -> List[Tuple[Message, Any]]: + return [(Message(text=input(self._prompt_request)), self._ctx_id)]
+ + +
+[docs] + def _respond(self, responses: List[Context]): + print(f"{self._prompt_response}{responses[0].last_response.text}", file=self._descriptor)
+ + +
+[docs] + async def connect(self, pipeline_runner: PipelineRunnerFunction, **kwargs): + """ + The CLIProvider generates new dialog id used to user identification on each `connect` call. + + :param pipeline_runner: A function that should return pipeline response to user request; + usually it's a :py:meth:`~dff.pipeline.pipeline.pipeline.Pipeline._run_pipeline` function. + :type pipeline_runner: PipelineRunnerFunction + :param \\**kwargs: argument, added for compatibility with super class, it shouldn't be used normally. + """ + self._ctx_id = uuid.uuid4() + if self._intro is not None: + print(self._intro) + await super().connect(pipeline_runner, **kwargs)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/messengers/telegram/interface.html b/_modules/dff/messengers/telegram/interface.html new file mode 100644 index 0000000000..dd648dfdea --- /dev/null +++ b/_modules/dff/messengers/telegram/interface.html @@ -0,0 +1,816 @@ + + + + + + + + + + dff.messengers.telegram.interface — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.messengers.telegram.interface

+"""
+Interface
+------------
+This module implements various interfaces for :py:class:`~dff.messengers.telegram.messenger.TelegramMessenger`
+that can be used to interact with the Telegram API.
+"""
+import asyncio
+from typing import Any, Optional, List, Tuple, Callable
+
+from telebot import types, apihelper
+
+from dff.messengers.common import MessengerInterface, PipelineRunnerFunction, CallbackMessengerInterface
+from .messenger import TelegramMessenger
+from .message import TelegramMessage
+
+try:
+    from flask import Flask, request, abort
+
+    flask_imported = True
+except ImportError:
+    flask_imported = False
+    Flask = Any
+    request, abort = None, None
+
+
+apihelper.ENABLE_MIDDLEWARE = True
+
+
+
+[docs] +def extract_telegram_request_and_id( + update: types.Update, messenger: Optional[TelegramMessenger] = None +) -> Tuple[TelegramMessage, int]: # pragma: no cover + """ + Utility function that extracts parameters from a telegram update. + Changes the messenger state, setting the last update id. + + Returned message has the following fields: + + - | `update_id` -- this field stores `update.update_id`, + - | `update` -- this field stores the first non-empty field of `update`, + - | `update_type` -- this field stores the name of the first non-empty field of `update`, + - | `text` -- this field stores `update.message.text`, + - | `callback_query` -- this field stores `update.callback_query.data`. + + Also return context id which is `chat`, `from_user` or `user` of the update. + + :param update: Update to process. + :param messenger: + Messenger instance. If passed updates `last_update_id`. + Defaults to None. + """ + if messenger is not None: + if update.update_id > messenger.last_update_id: + messenger.last_update_id = update.update_id + + message = TelegramMessage(update_id=update.update_id) + ctx_id = None + + for update_field, update_value in vars(update).items(): + if update_field != "update_id" and update_value is not None: + if message.update is not None: + raise RuntimeError(f"Two update fields. First: {message.update_type}; second: {update_field}") + message.update_type = update_field + message.update = update_value + if isinstance(update_value, types.Message): + message.text = update_value.text + + if isinstance(update_value, types.CallbackQuery): + data = update_value.data + if data is not None: + message.callback_query = data + + dict_update = vars(update_value) + # if 'chat' is not available, fall back to 'from_user', then to 'user' + user = dict_update.get("chat", dict_update.get("from_user", dict_update.get("user"))) + ctx_id = getattr(user, "id", None) + if message.update is None: + raise RuntimeError(f"No update fields found: {update}") + + return message, ctx_id
+ + + +
+[docs] +class PollingTelegramInterface(MessengerInterface): # pragma: no cover + """ + Telegram interface that retrieves updates by polling. + Multi-threaded polling is currently not supported. + + :param token: Bot token + :param messenger: + :py:class:`~dff.messengers.telegram.messenger.TelegramMessenger` instance. + If not `None` will be used instead of creating messenger from token. + Token value does not matter in that case. + Defaults to None. + :param interval: + Polling interval. See `link <https://github.com/eternnoir/pyTelegramBotAPI#telebot>`__. + Defaults to 2. + :param allowed_updates: + Processed updates. See `link <https://github.com/eternnoir/pyTelegramBotAPI#telebot>`__. + Defaults to None. + :param timeout: + General timeout. See `link <https://github.com/eternnoir/pyTelegramBotAPI#telebot>`__. + Defaults to 20. + :param long_polling_timeout: + Polling timeout. See `link <https://github.com/eternnoir/pyTelegramBotAPI#telebot>`__. + Defaults to 20. + """ + + def __init__( + self, + token: str, + interval: int = 2, + allowed_updates: Optional[List[str]] = None, + timeout: int = 20, + long_polling_timeout: int = 20, + messenger: Optional[TelegramMessenger] = None, + ): + self.messenger = ( + messenger if messenger is not None else TelegramMessenger(token, suppress_middleware_excepions=True) + ) + self.allowed_updates = allowed_updates + self.interval = interval + self.timeout = timeout + self.long_polling_timeout = long_polling_timeout + +
+[docs] + async def connect(self, callback: PipelineRunnerFunction, loop: Optional[Callable] = None, *args, **kwargs): + def dff_middleware(bot_instance, update): + message, ctx_id = extract_telegram_request_and_id(update, self.messenger) + + ctx = asyncio.run(callback(message, ctx_id)) + + bot_instance.send_response(ctx_id, ctx.last_response) + + self.messenger.middleware_handler()(dff_middleware) + + self.messenger.infinity_polling( + timeout=self.timeout, long_polling_timeout=self.long_polling_timeout, interval=self.interval + )
+
+ + + +
+[docs] +class CallbackTelegramInterface(CallbackMessengerInterface): # pragma: no cover + """ + Asynchronous Telegram interface that retrieves updates via webhook. + Any Flask server can be passed to set up a webhook on a separate endpoint. + + :param token: Bot token + :param messenger: + :py:class:`~dff.messengers.telegram.messenger.TelegramMessenger` instance. + If not `None` will be used instead of creating messenger from token. + Token value does not matter in that case. + Defaults to None. + :param app: + Flask instance. + Defaults to `Flask(__name__)`. + :param endpoint: + Webhook endpoint. Should be prefixed with "/". + Defaults to "/telegram-webhook". + :param host: + Host IP. + Defaults to "localhost". + :param port: + Port of the app. + Defaults to 8443. + :param debug: + Run the Flask app in debug mode. + :param load_dotenv: + Whether or not the .env file in the project folder + should be used to set environment variables. + :param full_uri: + Full public IP of your webhook that is accessible by https. + Defaults to `"https://{host}:{port}{endpoint}"`. + :param wsgi_options: + Keyword arguments to forward to `Flask.run` method. + Use these to set `ssl_context` and other WSGI options. + """ + + def __init__( + self, + token: str, + app: Optional[Flask] = None, + host: str = "localhost", + port: int = 8443, + debug: Optional[bool] = None, + load_dotenv: bool = True, + endpoint: str = "/telegram-webhook", + full_uri: Optional[str] = None, + messenger: Optional[TelegramMessenger] = None, + **wsgi_options, + ): + if not flask_imported: + raise ModuleNotFoundError("Flask is not installed. Install it with `pip install flask`.") + + self.messenger = messenger if messenger is not None else TelegramMessenger(token) + self.app = app if app else Flask(__name__) + self.host = host + self.port = port + self.debug = debug + self.load_dotenv = load_dotenv + self.wsgi_options = wsgi_options + self.endpoint = endpoint + self.full_uri = full_uri if full_uri is not None else "".join([f"https://{host}:{port}", endpoint]) + + async def endpoint(): + if not request.headers.get("content-type") == "application/json": + abort(403) + + json_string = request.get_data().decode("utf-8") + update = types.Update.de_json(json_string) + resp = await self.on_request_async(*extract_telegram_request_and_id(update, self.messenger)) + self.messenger.send_response(resp.id, resp.last_response) + return "" + + self.app.route(self.endpoint, methods=["POST"])(endpoint) + +
+[docs] + async def connect(self, callback: PipelineRunnerFunction): + await super().connect(callback) + + self.messenger.remove_webhook() + self.messenger.set_webhook(self.full_uri) + + self.app.run( + host=self.host, port=self.port, load_dotenv=self.load_dotenv, debug=self.debug, **self.wsgi_options + )
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/messengers/telegram/message.html b/_modules/dff/messengers/telegram/message.html new file mode 100644 index 0000000000..6c2d84ec1f --- /dev/null +++ b/_modules/dff/messengers/telegram/message.html @@ -0,0 +1,703 @@ + + + + + + + + + + dff.messengers.telegram.message — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.messengers.telegram.message

+"""
+Telegram Message
+----------------
+This module implements inherited classes :py:mod:`dff.script.core.message` modified for usage with Telegram.
+"""
+from typing import Optional, Union
+from enum import Enum
+
+from telebot.types import (
+    ReplyKeyboardRemove,
+    ReplyKeyboardMarkup,
+    InlineKeyboardMarkup,
+    Message as tlMessage,
+    InlineQuery,
+    ChosenInlineResult,
+    CallbackQuery as tlCallbackQuery,
+    ShippingQuery,
+    PreCheckoutQuery,
+    Poll,
+    PollAnswer,
+    ChatMemberUpdated,
+    ChatJoinRequest,
+)
+
+from dff.script.core.message import Message, Location, Keyboard, DataModel
+from pydantic import model_validator
+
+
+
+[docs] +class TelegramUI(Keyboard): + is_inline: bool = True + """ + Whether to use `inline keyboard <https://core.telegram.org/bots/features#inline-keyboards>`__ or + a `keyboard <https://core.telegram.org/bots/features#keyboards>`__. + """ + row_width: int = 3 + """Limits the maximum number of buttons in a row.""" + +
+[docs] + @model_validator(mode="after") + def validate_buttons(self, _): + if not self.is_inline: + for button in self.buttons: + if button.payload is not None or button.source is not None: + raise AssertionError(f"`payload` and `source` are only used for inline keyboards: {button}") + return self
+
+ + + +
+[docs] +class _ClickButton(DataModel): + """This class is only used in telegram tests (to click buttons as a client).""" + + button_index: int
+ + + +
+[docs] +class RemoveKeyboard(DataModel): + """Pass an instance of this class to :py:attr:`~.TelegramMessage.ui` to remove current keyboard.""" + + ...
+ + + +
+[docs] +class ParseMode(Enum): + """ + Parse mode of the message. + More info: https://core.telegram.org/bots/api#formatting-options. + """ + + HTML = "HTML" + MARKDOWN = "MarkdownV2"
+ + + +
+[docs] +class TelegramMessage(Message): + ui: Optional[ + Union[TelegramUI, RemoveKeyboard, ReplyKeyboardRemove, ReplyKeyboardMarkup, InlineKeyboardMarkup] + ] = None + location: Optional[Location] = None + callback_query: Optional[Union[str, _ClickButton]] = None + update: Optional[ + Union[ + tlMessage, + InlineQuery, + ChosenInlineResult, + tlCallbackQuery, + ShippingQuery, + PreCheckoutQuery, + Poll, + PollAnswer, + ChatMemberUpdated, + ChatJoinRequest, + ] + ] = None + """This field stores an update representing this message.""" + update_id: Optional[int] = None + update_type: Optional[str] = None + """Name of the field that stores an update representing this message.""" + parse_mode: Optional[ParseMode] = None + """Parse mode of the message.""" + + def __eq__(self, other): + if isinstance(other, Message): + for field in self.model_fields: + if field not in ("parse_mode", "update_id", "update", "update_type"): + if field not in other.model_fields: + return False + if self.__getattribute__(field) != other.__getattribute__(field): + return False + return True + return NotImplemented
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/messengers/telegram/messenger.html b/_modules/dff/messengers/telegram/messenger.html new file mode 100644 index 0000000000..f5fb8b9aa9 --- /dev/null +++ b/_modules/dff/messengers/telegram/messenger.html @@ -0,0 +1,839 @@ + + + + + + + + + + dff.messengers.telegram.messenger — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.messengers.telegram.messenger

+"""
+Messenger
+-----------------
+The Messenger module provides the :py:class:`~dff.messengers.telegram.messenger.TelegramMessenger` class.
+The former inherits from the :py:class:`~TeleBot` class from the `pytelegrambotapi` library.
+Using it, you can put Telegram update handlers inside your script and condition your transitions accordingly.
+
+"""
+from pathlib import Path
+from typing import Union, List, Optional, Callable
+from enum import Enum
+
+from telebot import types, TeleBot
+
+from dff.script import Context
+from dff.pipeline import Pipeline
+
+from .utils import batch_open_io
+from .message import TelegramMessage, TelegramUI, RemoveKeyboard
+
+from dff.script import Message
+from dff.script.core.message import Audio, Video, Image, Document
+
+
+
+[docs] +class TelegramMessenger(TeleBot): # pragma: no cover + """ + This class inherits from `Telebot` and implements framework-specific functionality + like sending generic responses. + + :param token: A Telegram API bot token. + :param kwargs: Arbitrary parameters that match the signature of the `Telebot` class. + For reference see: `link <https://github.com/eternnoir/pyTelegramBotAPI#telebot>`_ . + + """ + + def __init__( + self, + token: str, + **kwargs, + ): + super().__init__(token, threaded=False, **kwargs) + +
+[docs] + def send_response(self, chat_id: Union[str, int], response: Union[str, dict, Message]) -> None: + """ + Cast `response` to :py:class:`~dff.messengers.telegram.types.TelegramMessage` and send it. + Message fields are sent in separate messages in the following order: + + 1. Attachments + 2. Location + 3. Text with keyboard + + :param chat_id: Telegram chat ID. + :param response: Response data. String, dictionary or :py:class:`~dff.script.responses.generics.Response`. + will be cast to :py:class:`~dff.messengers.telegram.types.TelegramMessage`. + """ + if isinstance(response, TelegramMessage): + ready_response = response + elif isinstance(response, str): + ready_response = TelegramMessage(text=response) + elif isinstance(response, Message): + ready_response = TelegramMessage.model_validate(response.model_dump()) + elif isinstance(response, dict): + ready_response = TelegramMessage.model_validate(response) + else: + raise TypeError( + "Type of the response argument should be one of the following:" + " `str`, `dict`, `Message`, or `TelegramMessage`." + ) + parse_mode = ready_response.parse_mode.value if ready_response.parse_mode is not None else None + if ready_response.attachments is not None: + if len(ready_response.attachments.files) == 1: + attachment = ready_response.attachments.files[0] + if isinstance(attachment, Audio): + method = self.send_audio + elif isinstance(attachment, Document): + method = self.send_document + elif isinstance(attachment, Video): + method = self.send_video + elif isinstance(attachment, Image): + method = self.send_photo + else: + raise TypeError(type(attachment)) + params = {"caption": attachment.title, "parse_mode": parse_mode} + if isinstance(attachment.source, Path): + with open(attachment.source, "rb") as file: + method(chat_id, file, **params) + else: + method(chat_id, str(attachment.source or attachment.id), **params) + else: + + def cast(file): + if isinstance(file, Image): + cast_to_media_type = types.InputMediaPhoto + elif isinstance(file, Audio): + cast_to_media_type = types.InputMediaAudio + elif isinstance(file, Document): + cast_to_media_type = types.InputMediaDocument + elif isinstance(file, Video): + cast_to_media_type = types.InputMediaVideo + else: + raise TypeError(type(file)) + return cast_to_media_type(media=str(file.source or file.id), caption=file.title) + + files = map(cast, ready_response.attachments.files) + with batch_open_io(files) as media: + self.send_media_group(chat_id=chat_id, media=media) + + if ready_response.location: + self.send_location( + chat_id=chat_id, + latitude=ready_response.location.latitude, + longitude=ready_response.location.longitude, + ) + + if ready_response.ui is not None: + if isinstance(ready_response.ui, RemoveKeyboard): + keyboard = types.ReplyKeyboardRemove() + elif isinstance(ready_response.ui, TelegramUI): + if ready_response.ui.is_inline: + keyboard = types.InlineKeyboardMarkup(row_width=ready_response.ui.row_width) + buttons = [ + types.InlineKeyboardButton( + text=item.text, + url=item.source, + callback_data=item.payload, + ) + for item in ready_response.ui.buttons + ] + else: + keyboard = types.ReplyKeyboardMarkup(row_width=ready_response.ui.row_width) + buttons = [ + types.KeyboardButton( + text=item.text, + ) + for item in ready_response.ui.buttons + ] + keyboard.add(*buttons, row_width=ready_response.ui.row_width) + else: + keyboard = ready_response.ui + else: + keyboard = None + + if ready_response.text is not None: + self.send_message( + chat_id=chat_id, + text=ready_response.text, + reply_markup=keyboard, + parse_mode=parse_mode, + ) + elif keyboard is not None: + self.send_message( + chat_id=chat_id, + text="", + reply_markup=keyboard, + parse_mode=parse_mode, + )
+
+ + + +_default_messenger = TeleBot("") + + +
+[docs] +class UpdateType(Enum): + """ + Represents a type of the telegram update + (which field contains an update in :py:class:`telebot.types.Update`). + See `link <https://pytba.readthedocs.io/en/latest/types.html#telebot.types.Update>`__. + """ + + ALL = "ALL" + MESSAGE = "message" + EDITED_MESSAGE = "edited_message" + CHANNEL_POST = "channel_post" + EDITED_CHANNEL_POST = "edited_channel_post" + INLINE_QUERY = "inline_query" + CHOSEN_INLINE_RESULT = "chosen_inline_result" + CALLBACK_QUERY = "callback_query" + SHIPPING_QUERY = "shipping_query" + PRE_CHECKOUT_QUERY = "pre_checkout_query" + POLL = "poll" + POLL_ANSWER = "poll_answer" + MY_CHAT_MEMBER = "my_chat_member" + CHAT_MEMBER = "chat_member" + CHAT_JOIN_REQUEST = "chat_join_request"
+ + + +
+[docs] +def telegram_condition( + messenger: TeleBot = _default_messenger, + update_type: UpdateType = UpdateType.MESSAGE, + commands: Optional[List[str]] = None, + regexp: Optional[str] = None, + func: Optional[Callable] = None, + content_types: Optional[List[str]] = None, + chat_types: Optional[List[str]] = None, + **kwargs, +): + """ + A condition triggered by updates that match the given parameters. + + :param messenger: + Messenger to test filters on. Used only for :py:attr:`Telebot.custom_filters`. + Defaults to :py:data:`._default_messenger`. + :param update_type: + If set to any `UpdateType` other than `UpdateType.ALL` + it will check that an update is of the same type. + Defaults to `UpdateType.Message`. + :param commands: + Telegram command trigger. + See `link <https://github.com/eternnoir/pyTelegramBotAPI#general-api-documentation>`__. + :param regexp: + Regex trigger. + See `link <https://github.com/eternnoir/pyTelegramBotAPI#general-api-documentation>`__. + :param func: + Callable trigger. + See `link <https://github.com/eternnoir/pyTelegramBotAPI#general-api-documentation>`__. + :param content_types: + Content type trigger. + See `link <https://github.com/eternnoir/pyTelegramBotAPI#general-api-documentation>`__. + :param chat_types: + Chat type trigger. + See `link <https://github.com/eternnoir/pyTelegramBotAPI#general-api-documentation>`__. + """ + + update_handler = messenger._build_handler_dict( + None, + False, + commands=commands, + regexp=regexp, + func=func, + content_types=content_types, + chat_types=chat_types, + **kwargs, + ) + + def condition(ctx: Context, _: Pipeline, *__, **___): # pragma: no cover + last_request = ctx.last_request + if last_request is None: + return False + update = getattr(last_request, "update", None) + request_update_type = getattr(last_request, "update_type", None) + if update is None: + return False + if update_type != UpdateType.ALL and request_update_type != update_type.value: + return False + test_result = messenger._test_message_handler(update_handler, update) + return test_result + + return condition
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/messengers/telegram/utils.html b/_modules/dff/messengers/telegram/utils.html new file mode 100644 index 0000000000..f93ca3d6dc --- /dev/null +++ b/_modules/dff/messengers/telegram/utils.html @@ -0,0 +1,643 @@ + + + + + + + + + + dff.messengers.telegram.utils — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.messengers.telegram.utils

+"""
+Utils
+------
+This module contains utilities for connecting to Telegram.
+"""
+from typing import Union, Iterable
+from contextlib import contextmanager
+from pathlib import Path
+from io import IOBase
+
+from telebot import types
+
+
+
+[docs] +def open_io(item: types.InputMedia): + """ + Returns `InputMedia` with an opened file descriptor instead of path. + + :param item: InputMedia object. + """ + if isinstance(item.media, Path): + item.media = item.media.open(mode="rb") + return item
+ + + +
+[docs] +def close_io(item: types.InputMedia): + """ + Closes an IO in an `InputMedia` object to perform the cleanup. + + :param item: InputMedia object. + """ + if isinstance(item.media, IOBase): + item.media.close()
+ + + +
+[docs] +@contextmanager +def batch_open_io(item: Union[types.InputMedia, Iterable[types.InputMedia]]): + """ + Context manager that controls the state of file descriptors inside `InputMedia`. + Can be used both for single objects and collections. + + :param item: InputMedia objects that contain file descriptors. + """ + if isinstance(item, Iterable): + resources = list(map(open_io, item)) + else: + resources = open_io(item) + try: + yield resources + finally: + if isinstance(resources, Iterable): + for resource in resources: + close_io(resource) + else: + close_io(resources)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/conditions.html b/_modules/dff/pipeline/conditions.html new file mode 100644 index 0000000000..6bde2def9f --- /dev/null +++ b/_modules/dff/pipeline/conditions.html @@ -0,0 +1,693 @@ + + + + + + + + + + dff.pipeline.conditions — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.conditions

+"""
+Conditions
+----------
+The conditions module contains functions that can be used to determine whether the pipeline component to which they
+are attached should be executed or not.
+The standard set of them allows user to setup dependencies between pipeline components.
+"""
+from typing import Optional, ForwardRef
+
+from dff.script import Context
+
+from .types import (
+    PIPELINE_STATE_KEY,
+    StartConditionCheckerFunction,
+    ComponentExecutionState,
+    StartConditionCheckerAggregationFunction,
+)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +def always_start_condition(_: Context, __: Pipeline) -> bool: + """ + Condition that always allows service execution. It's the default condition for all services. + + :param _: Current dialog context. + :param __: Pipeline. + """ + return True
+ + + +
+[docs] +def service_successful_condition(path: Optional[str] = None) -> StartConditionCheckerFunction: + """ + Condition that allows service execution, only if the other service was executed successfully. + Returns :py:data:`~.StartConditionCheckerFunction`. + + :param path: The path of the condition pipeline component. + """ + + def check_service_state(ctx: Context, _: Pipeline): + state = ctx.framework_states[PIPELINE_STATE_KEY].get(path, ComponentExecutionState.NOT_RUN) + return ComponentExecutionState[state] == ComponentExecutionState.FINISHED + + return check_service_state
+ + + +
+[docs] +def not_condition(func: StartConditionCheckerFunction) -> StartConditionCheckerFunction: + """ + Condition that returns opposite boolean value to the one returned by incoming function. + Returns :py:data:`~.StartConditionCheckerFunction`. + + :param func: The function to return opposite of. + """ + + def not_function(ctx: Context, pipeline: Pipeline): + return not func(ctx, pipeline) + + return not_function
+ + + +
+[docs] +def aggregate_condition( + aggregator: StartConditionCheckerAggregationFunction, *functions: StartConditionCheckerFunction +) -> StartConditionCheckerFunction: + """ + Condition that returns aggregated boolean value from all booleans returned by incoming functions. + Returns :py:data:`~.StartConditionCheckerFunction`. + + :param aggregator: The function that accepts list of booleans and returns a single boolean. + :param functions: Functions to aggregate. + """ + + def aggregation_function(ctx: Context, pipeline: Pipeline): + return aggregator([func(ctx, pipeline) for func in functions]) + + return aggregation_function
+ + + +
+[docs] +def all_condition(*functions: StartConditionCheckerFunction) -> StartConditionCheckerFunction: + """ + Condition that returns `True` only if all incoming functions return `True`. + Returns :py:data:`~.StartConditionCheckerFunction`. + + :param functions: Functions to aggregate. + """ + return aggregate_condition(all, *functions)
+ + + +
+[docs] +def any_condition(*functions: StartConditionCheckerFunction) -> StartConditionCheckerFunction: + """ + Condition that returns `True` if any of incoming functions returns `True`. + Returns :py:data:`~.StartConditionCheckerFunction`. + + :param functions: Functions to aggregate. + """ + return aggregate_condition(any, *functions)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/pipeline/actor.html b/_modules/dff/pipeline/pipeline/actor.html new file mode 100644 index 0000000000..0773ea180e --- /dev/null +++ b/_modules/dff/pipeline/pipeline/actor.html @@ -0,0 +1,1031 @@ + + + + + + + + + + dff.pipeline.pipeline.actor — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.pipeline.actor

+"""
+Actor
+-----
+Actor is a component of :py:class:`.Pipeline`, that contains the :py:class:`.Script` and handles it.
+It is responsible for processing user input and determining the appropriate response based
+on the current state of the conversation and the script.
+The actor receives requests in the form of a :py:class:`.Context` class, which contains
+information about the user's input, the current state of the conversation, and other relevant data.
+
+The actor uses the dialog graph, represented by the :py:class:`.Script` class,
+to determine the appropriate response. The script contains the structure of the conversation,
+including the different `nodes` and `transitions`.
+It defines the possible paths that the conversation can take, and the conditions that must be met
+for a transition to occur. The actor uses this information to navigate the graph
+and determine the next step in the conversation.
+
+Overall, the actor acts as a bridge between the user's input and the dialog graph,
+making sure that the conversation follows the expected flow and providing a personalized experience to the user.
+
+Below you can see a diagram of user request processing with Actor.
+Both `request` and `response` are saved to :py:class:`.Context`.
+
+.. figure:: /_static/drawio/dfe/user_actor.png
+"""
+import logging
+from typing import Union, Callable, Optional, Dict, List, Any, ForwardRef
+import copy
+
+from dff.utils.turn_caching import cache_clear
+from dff.script.core.types import ActorStage, NodeLabel2Type, NodeLabel3Type, LabelType
+from dff.script.core.message import Message
+
+from dff.script.core.context import Context
+from dff.script.core.script import Script, Node
+from dff.script.core.normalization import normalize_label, normalize_response
+from dff.script.core.keywords import GLOBAL, LOCAL
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +def error_handler(error_msgs: list, msg: str, exception: Optional[Exception] = None, logging_flag: bool = True): + """ + This function handles errors during :py:class:`~dff.script.Script` validation. + + :param error_msgs: List that contains error messages. :py:func:`~dff.script.error_handler` + adds every next error message to that list. + :param msg: Error message which is to be added into `error_msgs`. + :param exception: Invoked exception. If it has been set, it is used to obtain logging traceback. + Defaults to `None`. + :param logging_flag: The flag which defines whether logging is necessary. Defaults to `True`. + """ + error_msgs.append(msg) + if logging_flag: + logger.error(msg, exc_info=exception)
+ + + +
+[docs] +class Actor: + """ + The class which is used to process :py:class:`~dff.script.Context` + according to the :py:class:`~dff.script.Script`. + + :param script: The dialog scenario: a graph described by the :py:class:`.Keywords`. + While the graph is being initialized, it is validated and then used for the dialog. + :param start_label: The start node of :py:class:`~dff.script.Script`. The execution begins with it. + :param fallback_label: The label of :py:class:`~dff.script.Script`. + Dialog comes into that label if all other transitions failed, + or there was an error while executing the scenario. + Defaults to `None`. + :param label_priority: Default priority value for all :py:const:`labels <dff.script.NodeLabel3Type>` + where there is no priority. Defaults to `1.0`. + :param condition_handler: Handler that processes a call of condition functions. Defaults to `None`. + :param handlers: This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~dff.script.Actor`. + + - key (:py:class:`~dff.script.ActorStage`) - Stage in which the handler is called. + - value (List[Callable]) - The list of called handlers for each stage. Defaults to an empty `dict`. + """ + + def __init__( + self, + script: Union[Script, dict], + start_label: NodeLabel2Type, + fallback_label: Optional[NodeLabel2Type] = None, + label_priority: float = 1.0, + condition_handler: Optional[Callable] = None, + handlers: Optional[Dict[ActorStage, List[Callable]]] = None, + ): + # script validation + self.script = script if isinstance(script, Script) else Script(script=script) + self.label_priority = label_priority + + # node labels validation + self.start_label = normalize_label(start_label) + if self.script.get(self.start_label[0], {}).get(self.start_label[1]) is None: + raise ValueError(f"Unknown start_label={self.start_label}") + + if fallback_label is None: + self.fallback_label = self.start_label + else: + self.fallback_label = normalize_label(fallback_label) + if self.script.get(self.fallback_label[0], {}).get(self.fallback_label[1]) is None: + raise ValueError(f"Unknown fallback_label={self.fallback_label}") + self.condition_handler = default_condition_handler if condition_handler is None else condition_handler + + self.handlers = {} if handlers is None else handlers + + # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! + self._clean_turn_cache = True + + def __call__( + self, pipeline: Pipeline, ctx: Optional[Union[Context, dict, str]] = None, *args, **kwargs + ) -> Union[Context, dict, str]: + # context init + ctx = self._context_init(ctx, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.CONTEXT_INIT, *args, **kwargs) + + # get previous node + ctx = self._get_previous_node(ctx, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.GET_PREVIOUS_NODE, *args, **kwargs) + + # rewrite previous node + ctx = self._rewrite_previous_node(ctx, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.REWRITE_PREVIOUS_NODE, *args, **kwargs) + + # run pre transitions processing + ctx = self._run_pre_transitions_processing(ctx, pipeline, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.RUN_PRE_TRANSITIONS_PROCESSING, *args, **kwargs) + + # get true labels for scopes (GLOBAL, LOCAL, NODE) + ctx = self._get_true_labels(ctx, pipeline, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.GET_TRUE_LABELS, *args, **kwargs) + + # get next node + ctx = self._get_next_node(ctx, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.GET_NEXT_NODE, *args, **kwargs) + + ctx.add_label(ctx.framework_states["actor"]["next_label"][:2]) + + # rewrite next node + ctx = self._rewrite_next_node(ctx, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.REWRITE_NEXT_NODE, *args, **kwargs) + + # run pre response processing + ctx = self._run_pre_response_processing(ctx, pipeline, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.RUN_PRE_RESPONSE_PROCESSING, *args, **kwargs) + + # create response + ctx.framework_states["actor"]["response"] = ctx.framework_states["actor"][ + "pre_response_processed_node" + ].run_response(ctx, pipeline, *args, **kwargs) + self._run_handlers(ctx, pipeline, ActorStage.CREATE_RESPONSE, *args, **kwargs) + ctx.add_response(ctx.framework_states["actor"]["response"]) + + self._run_handlers(ctx, pipeline, ActorStage.FINISH_TURN, *args, **kwargs) + if self._clean_turn_cache: + cache_clear() + + del ctx.framework_states["actor"] + return ctx + +
+[docs] + @staticmethod + def _context_init(ctx: Optional[Union[Context, dict, str]] = None, *args, **kwargs) -> Context: + ctx = Context.cast(ctx) + if not ctx.requests: + ctx.add_request(Message()) + ctx.framework_states["actor"] = {} + return ctx
+ + +
+[docs] + def _get_previous_node(self, ctx: Context, *args, **kwargs) -> Context: + ctx.framework_states["actor"]["previous_label"] = ( + normalize_label(ctx.last_label) if ctx.last_label else self.start_label + ) + ctx.framework_states["actor"]["previous_node"] = self.script.get( + ctx.framework_states["actor"]["previous_label"][0], {} + ).get(ctx.framework_states["actor"]["previous_label"][1], Node()) + return ctx
+ + +
+[docs] + def _get_true_labels(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + # GLOBAL + ctx.framework_states["actor"]["global_transitions"] = ( + self.script.get(GLOBAL, {}).get(GLOBAL, Node()).transitions + ) + ctx.framework_states["actor"]["global_true_label"] = self._get_true_label( + ctx.framework_states["actor"]["global_transitions"], ctx, pipeline, GLOBAL, "global" + ) + + # LOCAL + ctx.framework_states["actor"]["local_transitions"] = ( + self.script.get(ctx.framework_states["actor"]["previous_label"][0], {}).get(LOCAL, Node()).transitions + ) + ctx.framework_states["actor"]["local_true_label"] = self._get_true_label( + ctx.framework_states["actor"]["local_transitions"], + ctx, + pipeline, + ctx.framework_states["actor"]["previous_label"][0], + "local", + ) + + # NODE + ctx.framework_states["actor"]["node_transitions"] = ctx.framework_states["actor"][ + "pre_transitions_processed_node" + ].transitions + ctx.framework_states["actor"]["node_true_label"] = self._get_true_label( + ctx.framework_states["actor"]["node_transitions"], + ctx, + pipeline, + ctx.framework_states["actor"]["previous_label"][0], + "node", + ) + return ctx
+ + +
+[docs] + def _get_next_node(self, ctx: Context, *args, **kwargs) -> Context: + # choose next label + ctx.framework_states["actor"]["next_label"] = self._choose_label( + ctx.framework_states["actor"]["node_true_label"], ctx.framework_states["actor"]["local_true_label"] + ) + ctx.framework_states["actor"]["next_label"] = self._choose_label( + ctx.framework_states["actor"]["next_label"], ctx.framework_states["actor"]["global_true_label"] + ) + # get next node + ctx.framework_states["actor"]["next_node"] = self.script.get( + ctx.framework_states["actor"]["next_label"][0], {} + ).get(ctx.framework_states["actor"]["next_label"][1]) + return ctx
+ + +
+[docs] + def _rewrite_previous_node(self, ctx: Context, *args, **kwargs) -> Context: + node = ctx.framework_states["actor"]["previous_node"] + flow_label = ctx.framework_states["actor"]["previous_label"][0] + ctx.framework_states["actor"]["previous_node"] = self._overwrite_node( + node, + flow_label, + only_current_node_transitions=True, + ) + return ctx
+ + +
+[docs] + def _rewrite_next_node(self, ctx: Context, *args, **kwargs) -> Context: + node = ctx.framework_states["actor"]["next_node"] + flow_label = ctx.framework_states["actor"]["next_label"][0] + ctx.framework_states["actor"]["next_node"] = self._overwrite_node(node, flow_label) + return ctx
+ + +
+[docs] + def _overwrite_node( + self, + current_node: Node, + flow_label: LabelType, + *args, + only_current_node_transitions: bool = False, + **kwargs, + ) -> Node: + overwritten_node = copy.deepcopy(self.script.get(GLOBAL, {}).get(GLOBAL, Node())) + local_node = self.script.get(flow_label, {}).get(LOCAL, Node()) + for node in [local_node, current_node]: + overwritten_node.pre_transitions_processing.update(node.pre_transitions_processing) + overwritten_node.pre_response_processing.update(node.pre_response_processing) + overwritten_node.response = overwritten_node.response if node.response is None else node.response + overwritten_node.misc.update(node.misc) + if not only_current_node_transitions: + overwritten_node.transitions.update(node.transitions) + if only_current_node_transitions: + overwritten_node.transitions = current_node.transitions + return overwritten_node
+ + +
+[docs] + def _run_pre_transitions_processing(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + ctx.framework_states["actor"]["processed_node"] = copy.deepcopy(ctx.framework_states["actor"]["previous_node"]) + ctx = ctx.framework_states["actor"]["previous_node"].run_pre_transitions_processing( + ctx, pipeline, *args, **kwargs + ) + ctx.framework_states["actor"]["pre_transitions_processed_node"] = ctx.framework_states["actor"][ + "processed_node" + ] + del ctx.framework_states["actor"]["processed_node"] + return ctx
+ + +
+[docs] + def _run_pre_response_processing(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + ctx.framework_states["actor"]["processed_node"] = copy.deepcopy(ctx.framework_states["actor"]["next_node"]) + ctx = ctx.framework_states["actor"]["next_node"].run_pre_response_processing(ctx, pipeline, *args, **kwargs) + ctx.framework_states["actor"]["pre_response_processed_node"] = ctx.framework_states["actor"]["processed_node"] + del ctx.framework_states["actor"]["processed_node"] + return ctx
+ + +
+[docs] + def _get_true_label( + self, + transitions: dict, + ctx: Context, + pipeline: Pipeline, + flow_label: LabelType, + transition_info: str = "", + *args, + **kwargs, + ) -> Optional[NodeLabel3Type]: + true_labels = [] + for label, condition in transitions.items(): + if self.condition_handler(condition, ctx, pipeline, *args, **kwargs): + if callable(label): + label = label(ctx, pipeline, *args, **kwargs) + # TODO: explicit handling of errors + if label is None: + continue + label = normalize_label(label, flow_label) + true_labels += [label] + true_labels = [ + ((label[0] if label[0] else flow_label),) + + label[1:2] + + ((self.label_priority if label[2] == float("-inf") else label[2]),) + for label in true_labels + ] + true_labels.sort(key=lambda label: -label[2]) + true_label = true_labels[0] if true_labels else None + logger.debug(f"{transition_info} transitions sorted by priority = {true_labels}") + return true_label
+ + +
+[docs] + def _run_handlers(self, ctx, pipeline: Pipeline, actor_stage: ActorStage, *args, **kwargs): + [handler(ctx, pipeline, *args, **kwargs) for handler in self.handlers.get(actor_stage, [])]
+ + +
+[docs] + def _choose_label( + self, specific_label: Optional[NodeLabel3Type], general_label: Optional[NodeLabel3Type] + ) -> NodeLabel3Type: + if all([specific_label, general_label]): + chosen_label = specific_label if specific_label[2] >= general_label[2] else general_label + elif any([specific_label, general_label]): + chosen_label = specific_label if specific_label else general_label + else: + chosen_label = self.fallback_label + return chosen_label
+ + +
+[docs] + def validate_script(self, pipeline: Pipeline, verbose: bool = True): + # TODO: script has to not contain priority == -inf, because it uses for miss values + flow_labels = [] + node_labels = [] + labels = [] + conditions = [] + for flow_name, flow in self.script.items(): + for node_name, node in flow.items(): + flow_labels += [flow_name] * len(node.transitions) + node_labels += [node_name] * len(node.transitions) + labels += list(node.transitions.keys()) + conditions += list(node.transitions.values()) + + error_msgs = [] + for flow_label, node_label, label, condition in zip(flow_labels, node_labels, labels, conditions): + ctx = Context() + ctx.validation = True + ctx.add_request(Message(text="text")) + + label = label(ctx, pipeline) if callable(label) else normalize_label(label, flow_label) + + # validate labeling + try: + node = self.script[label[0]][label[1]] + except Exception as exc: + msg = ( + f"Could not find node with label={label}, " + f"error was found in (flow_label, node_label)={(flow_label, node_label)}" + ) + error_handler(error_msgs, msg, exc, verbose) + break + + # validate responsing + response_func = normalize_response(node.response) + try: + response_result = response_func(ctx, pipeline) + if not isinstance(response_result, Message): + msg = ( + "Expected type of response_result is `Message`.\n" + + f"Got type(response_result)={type(response_result)}" + f" for label={label} , error was found in (flow_label, node_label)={(flow_label, node_label)}" + ) + error_handler(error_msgs, msg, None, verbose) + continue + except Exception as exc: + msg = ( + f"Got exception '''{exc}''' during response execution " + f"for label={label} and node.response={node.response}" + f", error was found in (flow_label, node_label)={(flow_label, node_label)}" + ) + error_handler(error_msgs, msg, exc, verbose) + continue + + # validate conditioning + try: + condition_result = condition(ctx, pipeline) + if not isinstance(condition(ctx, pipeline), bool): + raise Exception(f"Returned condition_result={condition_result}, but expected bool type") + except Exception as exc: + msg = f"Got exception '''{exc}''' during condition execution for label={label}" + error_handler(error_msgs, msg, exc, verbose) + continue + return error_msgs
+
+ + + +
+[docs] +def default_condition_handler( + condition: Callable, ctx: Context, pipeline: Pipeline, *args, **kwargs +) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + The simplest and quickest condition handler for trivial condition handling returns the callable condition: + + :param condition: Condition to copy. + :param ctx: Context of current condition. + :param pipeline: Pipeline we use in this condition. + """ + return condition(ctx, pipeline, *args, **kwargs)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/pipeline/component.html b/_modules/dff/pipeline/pipeline/component.html new file mode 100644 index 0000000000..1a6ada31b2 --- /dev/null +++ b/_modules/dff/pipeline/pipeline/component.html @@ -0,0 +1,843 @@ + + + + + + + + + + dff.pipeline.pipeline.component — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.pipeline.component

+"""
+Component
+---------
+The Component module defines a :py:class:`.PipelineComponent` class,
+which is a fundamental building block of the framework. A PipelineComponent represents a single
+step in a processing pipeline, and is responsible for performing a specific task or set of tasks.
+
+The PipelineComponent class can be a group or a service. It is designed to be reusable and composable,
+allowing developers to create complex processing pipelines by combining multiple components.
+"""
+import logging
+import abc
+import asyncio
+import copy
+from typing import Optional, Union, Awaitable, ForwardRef
+
+from dff.script import Context
+
+from ..service.extra import BeforeHandler, AfterHandler
+from ..conditions import always_start_condition
+from ..types import (
+    PIPELINE_STATE_KEY,
+    StartConditionCheckerFunction,
+    ComponentExecutionState,
+    ServiceRuntimeInfo,
+    GlobalExtraHandlerType,
+    ExtraHandlerFunction,
+    ExtraHandlerType,
+    ExtraHandlerBuilder,
+)
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +class PipelineComponent(abc.ABC): + """ + This class represents a pipeline component, which is a service or a service group. + It contains some fields that they have in common. + + :param before_handler: :py:class:`~.BeforeHandler`, associated with this component. + :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param after_handler: :py:class:`~.AfterHandler`, associated with this component. + :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), + if it exceeds this time, it is interrupted. + :param requested_async_flag: Requested asynchronous property; + if not defined, `calculated_async_flag` is used instead. + :param calculated_async_flag: Whether the component can be asynchronous or not + 1) for :py:class:`~.pipeline.service.service.Service`: whether its `handler` is asynchronous or not, + 2) for :py:class:`~.pipeline.service.group.ServiceGroup`: whether all its `services` are asynchronous or not. + + :param start_condition: StartConditionCheckerFunction that is invoked before each component execution; + component is executed only if it returns `True`. + :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`] + :param name: Component name (should be unique in single :py:class:`~.pipeline.service.group.ServiceGroup`), + should not be blank or contain `.` symbol. + :param path: Separated by dots path to component, is universally unique. + """ + + def __init__( + self, + before_handler: Optional[ExtraHandlerBuilder] = None, + after_handler: Optional[ExtraHandlerBuilder] = None, + timeout: Optional[float] = None, + requested_async_flag: Optional[bool] = None, + calculated_async_flag: bool = False, + start_condition: Optional[StartConditionCheckerFunction] = None, + name: Optional[str] = None, + path: Optional[str] = None, + ): + self.timeout = timeout + """ + Maximum component execution time (in seconds), + if it exceeds this time, it is interrupted (for asynchronous only!). + """ + self.requested_async_flag = requested_async_flag + """Requested asynchronous property; if not defined, :py:attr:`~requested_async_flag` is used instead.""" + self.calculated_async_flag = calculated_async_flag + """Calculated asynchronous property, whether the component can be asynchronous or not.""" + self.start_condition = always_start_condition if start_condition is None else start_condition + """ + Component start condition that is invoked before each component execution; + component is executed only if it returns `True`. + """ + self.name = name + """ + Component name (should be unique in single :py:class:`~pipeline.service.group.ServiceGroup`), + should not be blank or contain '.' symbol. + """ + self.path = path + """ + Dot-separated path to component (is universally unique). + This attribute is set in :py:func:`~dff.pipeline.pipeline.utils.finalize_service_group`. + """ + + self.before_handler = BeforeHandler([] if before_handler is None else before_handler) + self.after_handler = AfterHandler([] if after_handler is None else after_handler) + + if name is not None and (name == "" or "." in name): + raise Exception(f"User defined service name shouldn't be blank or contain '.' (service: {name})!") + + if not calculated_async_flag and requested_async_flag: + raise Exception(f"{type(self).__name__} '{name}' can't be asynchronous!") + +
+[docs] + def _set_state(self, ctx: Context, value: ComponentExecutionState): + """ + Method for component runtime state setting, state is preserved in `ctx.framework_states` dict, + in subdict, dedicated to this library. + + :param ctx: :py:class:`~.Context` to keep state in. + :param value: State to set. + :return: `None` + """ + if PIPELINE_STATE_KEY not in ctx.framework_states: + ctx.framework_states[PIPELINE_STATE_KEY] = {} + ctx.framework_states[PIPELINE_STATE_KEY][self.path] = value
+ + +
+[docs] + def get_state(self, ctx: Context, default: Optional[ComponentExecutionState] = None) -> ComponentExecutionState: + """ + Method for component runtime state getting, state is preserved in `ctx.framework_states` dict, + in subdict, dedicated to this library. + + :param ctx: :py:class:`~.Context` to get state from. + :param default: Default to return if no record found + (usually it's :py:attr:`~.pipeline.types.ComponentExecutionState.NOT_RUN`). + :return: :py:class:`~pipeline.types.ComponentExecutionState` of this service or default if not found. + """ + return ctx.framework_states[PIPELINE_STATE_KEY].get(self.path, default if default is not None else None)
+ + + @property + def asynchronous(self) -> bool: + """ + Property, that indicates, whether this component is synchronous or asynchronous. + It is calculated according to the following rules: + + - | If component **can** be asynchronous and :py:attr:`~requested_async_flag` is set, + it returns :py:attr:`~requested_async_flag`. + - | If component **can** be asynchronous and :py:attr:`~requested_async_flag` isn't set, + it returns `True`. + - | If component **can't** be asynchronous and :py:attr:`~requested_async_flag` is `False` or not set, + it returns `False`. + - | If component **can't** be asynchronous and :py:attr:`~requested_async_flag` is `True`, + an Exception is thrown in constructor. + + """ + return self.calculated_async_flag if self.requested_async_flag is None else self.requested_async_flag + +
+[docs] + async def run_extra_handler(self, stage: ExtraHandlerType, ctx: Context, pipeline: Pipeline): + extra_handler = None + if stage == ExtraHandlerType.BEFORE: + extra_handler = self.before_handler + if stage == ExtraHandlerType.AFTER: + extra_handler = self.after_handler + if extra_handler is None: + return + try: + extra_handler_result = await extra_handler(ctx, pipeline, self._get_runtime_info(ctx)) + if extra_handler.asynchronous and isinstance(extra_handler_result, Awaitable): + await extra_handler_result + except asyncio.TimeoutError: + logger.warning(f"{type(self).__name__} '{self.name}' {extra_handler.stage} extra handler timed out!")
+ + +
+[docs] + @abc.abstractmethod + async def _run(self, ctx: Context, pipeline: Optional[Pipeline] = None) -> Optional[Context]: + """ + A method for running pipeline component, it is overridden in all its children. + This method is run after the component's timeout is set (if needed). + + :param ctx: Current dialog :py:class:`~.Context`. + :param pipeline: This :py:class:`~.Pipeline`. + :return: :py:class:`~.Context` if this is a synchronous service or `None`, + asynchronous services shouldn't modify :py:class:`~.Context`. + """ + raise NotImplementedError
+ + + async def __call__(self, ctx: Context, pipeline: Optional[Pipeline] = None) -> Optional[Union[Context, Awaitable]]: + """ + A method for calling pipeline components. + It sets up timeout if this component is asynchronous and executes it using :py:meth:`~._run` method. + + :param ctx: Current dialog :py:class:`~.Context`. + :param pipeline: This :py:class:`~.Pipeline`. + :return: :py:class:`~.Context` if this is a synchronous service or :py:class:`~.typing.const.Awaitable`, + asynchronous services shouldn't modify :py:class:`~.Context`. + """ + if self.asynchronous: + task = asyncio.create_task(self._run(ctx, pipeline)) + return asyncio.wait_for(task, timeout=self.timeout) + else: + return await self._run(ctx, pipeline) + +
+[docs] + def add_extra_handler(self, global_extra_handler_type: GlobalExtraHandlerType, extra_handler: ExtraHandlerFunction): + """ + Method for adding a global extra handler to this particular component. + + :param global_extra_handler_type: A type of extra handler to add. + :param extra_handler: A :py:class:`~.GlobalExtraHandlerType` to add to the component as an extra handler. + :type extra_handler: :py:data:`~.ExtraHandlerFunction` + :return: `None` + """ + target = ( + self.before_handler if global_extra_handler_type is GlobalExtraHandlerType.BEFORE else self.after_handler + ) + target.functions.append(extra_handler)
+ + +
+[docs] + def _get_runtime_info(self, ctx: Context) -> ServiceRuntimeInfo: + """ + Method for retrieving runtime info about this component. + + :param ctx: Current dialog :py:class:`~.Context`. + :return: :py:class:`~.dff.script.typing.ServiceRuntimeInfo` + object where all not set fields are replaced with `[None]`. + """ + return ServiceRuntimeInfo( + name=self.name if self.name is not None else "[None]", + path=self.path if self.path is not None else "[None]", + timeout=self.timeout, + asynchronous=self.asynchronous, + execution_state=copy.deepcopy(ctx.framework_states[PIPELINE_STATE_KEY]), + )
+ + + @property + def info_dict(self) -> dict: + """ + Property for retrieving info dictionary about this component. + All not set fields there are replaced with `[None]`. + + :return: Info dict, containing most important component public fields as well as its type. + """ + return { + "type": type(self).__name__, + "name": self.name, + "path": self.path if self.path is not None else "[None]", + "asynchronous": self.asynchronous, + "start_condition": self.start_condition.__name__, + "extra_handlers": { + "before": self.before_handler.info_dict, + "after": self.after_handler.info_dict, + }, + }
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/pipeline/pipeline.html b/_modules/dff/pipeline/pipeline/pipeline.html new file mode 100644 index 0000000000..5ef523ee5c --- /dev/null +++ b/_modules/dff/pipeline/pipeline/pipeline.html @@ -0,0 +1,974 @@ + + + + + + + + + + dff.pipeline.pipeline.pipeline — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.pipeline.pipeline

+"""
+Pipeline
+--------
+The Pipeline module contains the :py:class:`.Pipeline` class,
+which is a fundamental element of the DFF. The Pipeline class is responsible
+for managing and executing the various components (:py:class:`.PipelineComponent`)which make up
+the processing of messages from and to users.
+It provides a way to organize and structure the messages processing flow.
+The Pipeline class is designed to be highly customizable and configurable,
+allowing developers to add, remove, or modify the components that make up the messages processing flow.
+
+The Pipeline class is designed to be used in conjunction with the :py:class:`.PipelineComponent`
+class, which is defined in the Component module. Together, these classes provide a powerful and flexible way
+to structure and manage the messages processing flow.
+"""
+import asyncio
+import logging
+from typing import Union, List, Dict, Optional, Hashable, Callable
+
+from dff.context_storages import DBContextStorage
+from dff.script import Script, Context, ActorStage
+from dff.script import NodeLabel2Type, Message
+from dff.utils.turn_caching import cache_clear
+
+from dff.messengers.common import MessengerInterface, CLIMessengerInterface
+from ..service.group import ServiceGroup
+from ..types import (
+    ServiceBuilder,
+    ServiceGroupBuilder,
+    PipelineBuilder,
+    GlobalExtraHandlerType,
+    ExtraHandlerFunction,
+    ExtraHandlerBuilder,
+)
+from ..types import PIPELINE_STATE_KEY
+from .utils import finalize_service_group, pretty_format_component_info_dict
+from dff.pipeline.pipeline.actor import Actor
+
+logger = logging.getLogger(__name__)
+
+ACTOR = "ACTOR"
+
+
+
+[docs] +class Pipeline: + """ + Class that automates service execution and creates service pipeline. + It accepts constructor parameters: + + :param script: (required) A :py:class:`~.Script` instance (object or dict). + :param start_label: (required) Actor start label. + :param fallback_label: Actor fallback label. + :param label_priority: Default priority value for all actor :py:const:`labels <dff.script.NodeLabel3Type>` + where there is no priority. Defaults to `1.0`. + :param validation_stage: This flag sets whether the validation stage is executed after actor creation. + It is executed by default. Defaults to `None`. + :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. + :param verbose: If it is `True`, logging is used in actor. Defaults to `True`. + :param handlers: This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~dff.script.Actor`. + + - key: :py:class:`~dff.script.ActorStage` - Stage in which the handler is called. + - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + + :param messenger_interface: An `AbsMessagingInterface` instance for this pipeline. + :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline or + a dict to store dialog :py:class:`~.Context`. + :param services: (required) A :py:data:`~.ServiceGroupBuilder` object, + that will be transformed to root service group. It should include :py:class:`~.Actor`, + but only once (raises exception otherwise). It will always be named pipeline. + :param wrappers: List of wrappers to add to pipeline root service group. + :param timeout: Timeout to add to pipeline root service group. + :param optimization_warnings: Asynchronous pipeline optimization check request flag; + warnings will be sent to logs. Additionally it has some calculated fields: + + - `_services_pipeline` is a pipeline root :py:class:`~.ServiceGroup` object, + - `actor` is a pipeline actor, found among services. + + """ + + def __init__( + self, + components: ServiceGroupBuilder, + script: Union[Script, Dict], + start_label: NodeLabel2Type, + fallback_label: Optional[NodeLabel2Type] = None, + label_priority: float = 1.0, + validation_stage: Optional[bool] = None, + condition_handler: Optional[Callable] = None, + verbose: bool = True, + handlers: Optional[Dict[ActorStage, List[Callable]]] = None, + messenger_interface: Optional[MessengerInterface] = None, + context_storage: Optional[Union[DBContextStorage, Dict]] = None, + before_handler: Optional[ExtraHandlerBuilder] = None, + after_handler: Optional[ExtraHandlerBuilder] = None, + timeout: Optional[float] = None, + optimization_warnings: bool = False, + ): + self.actor: Actor = None + self.messenger_interface = CLIMessengerInterface() if messenger_interface is None else messenger_interface + self.context_storage = {} if context_storage is None else context_storage + self._services_pipeline = ServiceGroup( + components, + before_handler=before_handler, + after_handler=after_handler, + timeout=timeout, + ) + + self._services_pipeline.name = "pipeline" + self._services_pipeline.path = ".pipeline" + actor_exists = finalize_service_group(self._services_pipeline, path=self._services_pipeline.path) + if not actor_exists: + raise Exception("Actor not found in pipeline!") + else: + self.set_actor( + script, + start_label, + fallback_label, + label_priority, + validation_stage, + condition_handler, + verbose, + handlers, + ) + if self.actor is None: + raise Exception("Actor wasn't initialized correctly!") + + if optimization_warnings: + self._services_pipeline.log_optimization_warnings() + + # NB! The following API is highly experimental and may be removed at ANY time WITHOUT FURTHER NOTICE!! + self._clean_turn_cache = True + if self._clean_turn_cache: + self.actor._clean_turn_cache = False + +
+[docs] + def add_global_handler( + self, + global_handler_type: GlobalExtraHandlerType, + extra_handler: ExtraHandlerFunction, + whitelist: Optional[List[str]] = None, + blacklist: Optional[List[str]] = None, + ): + """ + Method for adding global wrappers to pipeline. + Different types of global wrappers are called before/after pipeline execution + or before/after each pipeline component. + They can be used for pipeline statistics collection or other functionality extensions. + NB! Global wrappers are still wrappers, + they shouldn't be used for much time-consuming tasks (see ../service/wrapper.py). + + :param global_handler_type: (required) indication where the wrapper + function should be executed. + :param extra_handler: (required) wrapper function itself. + :type extra_handler: ExtraHandlerFunction + :param whitelist: a list of services to only add this wrapper to. + :param blacklist: a list of services to not add this wrapper to. + :return: `None` + """ + + def condition(name: str) -> bool: + return (whitelist is None or name in whitelist) and (blacklist is None or name not in blacklist) + + if ( + global_handler_type is GlobalExtraHandlerType.BEFORE_ALL + or global_handler_type is GlobalExtraHandlerType.AFTER_ALL + ): + whitelist = ["pipeline"] + global_handler_type = ( + GlobalExtraHandlerType.BEFORE + if global_handler_type is GlobalExtraHandlerType.BEFORE_ALL + else GlobalExtraHandlerType.AFTER + ) + + self._services_pipeline.add_extra_handler(global_handler_type, extra_handler, condition)
+ + + @property + def info_dict(self) -> dict: + """ + Property for retrieving info dictionary about this pipeline. + Returns info dict, containing most important component public fields as well as its type. + All complex or unserializable fields here are replaced with 'Instance of [type]'. + """ + return { + "type": type(self).__name__, + "messenger_interface": f"Instance of {type(self.messenger_interface).__name__}", + "context_storage": f"Instance of {type(self.context_storage).__name__}", + "services": [self._services_pipeline.info_dict], + } + +
+[docs] + def pretty_format(self, show_extra_handlers: bool = False, indent: int = 4) -> str: + """ + Method for receiving pretty-formatted string description of the pipeline. + Resulting string structure is somewhat similar to YAML string. + Should be used in debugging/logging purposes and should not be parsed. + + :param show_wrappers: Whether to include Wrappers or not (could be many and/or generated). + :param indent: Offset from new line to add before component children. + """ + return pretty_format_component_info_dict(self.info_dict, show_extra_handlers, indent=indent)
+ + +
+[docs] + @classmethod + def from_script( + cls, + script: Union[Script, Dict], + start_label: NodeLabel2Type, + fallback_label: Optional[NodeLabel2Type] = None, + label_priority: float = 1.0, + validation_stage: Optional[bool] = None, + condition_handler: Optional[Callable] = None, + verbose: bool = True, + handlers: Optional[Dict[ActorStage, List[Callable]]] = None, + context_storage: Optional[Union[DBContextStorage, Dict]] = None, + messenger_interface: Optional[MessengerInterface] = None, + pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] = None, + post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] = None, + ) -> "Pipeline": + """ + Pipeline script-based constructor. + It creates :py:class:`~.Actor` object and wraps it with pipeline. + NB! It is generally not designed for projects with complex structure. + :py:class:`~.Service` and :py:class:`~.ServiceGroup` customization + becomes not as obvious as it could be with it. + Should be preferred for simple workflows with Actor auto-execution. + + :param script: (required) A :py:class:`~.Script` instance (object or dict). + :param start_label: (required) Actor start label. + :param fallback_label: Actor fallback label. + :param label_priority: Default priority value for all actor :py:const:`labels <dff.script.NodeLabel3Type>` + where there is no priority. Defaults to `1.0`. + :param validation_stage: This flag sets whether the validation stage is executed after actor creation. + It is executed by default. Defaults to `None`. + :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. + :param verbose: If it is `True`, logging is used in actor. Defaults to `True`. + :param handlers: This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~dff.script.Actor`. + + - key: :py:class:`~dff.script.ActorStage` - Stage in which the handler is called. + - value: List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + + :param context_storage: An :py:class:`~.DBContextStorage` instance for this pipeline + or a dict to store dialog :py:class:`~.Context`. + :param messenger_interface: An instance for this pipeline. + :param pre_services: List of :py:data:`~.ServiceBuilder` or + :py:data:`~.ServiceGroupBuilder` that will be executed before Actor. + :type pre_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] + :param post_services: List of :py:data:`~.ServiceBuilder` or + :py:data:`~.ServiceGroupBuilder` that will be executed after Actor. + It constructs root service group by merging `pre_services` + actor + `post_services`. + :type post_services: Optional[List[Union[ServiceBuilder, ServiceGroupBuilder]]] + """ + pre_services = [] if pre_services is None else pre_services + post_services = [] if post_services is None else post_services + return cls( + script=script, + start_label=start_label, + fallback_label=fallback_label, + label_priority=label_priority, + validation_stage=validation_stage, + condition_handler=condition_handler, + verbose=verbose, + handlers=handlers, + messenger_interface=messenger_interface, + context_storage=context_storage, + components=[*pre_services, ACTOR, *post_services], + )
+ + +
+[docs] + def set_actor( + self, + script: Union[Script, Dict], + start_label: NodeLabel2Type, + fallback_label: Optional[NodeLabel2Type] = None, + label_priority: float = 1.0, + validation_stage: Optional[bool] = None, + condition_handler: Optional[Callable] = None, + verbose: bool = True, + handlers: Optional[Dict[ActorStage, List[Callable]]] = None, + ): + """ + Set actor for the current pipeline and conducts necessary checks. + Reset actor to previous if any errors are found. + + :param script: (required) A :py:class:`~.Script` instance (object or dict). + :param start_label: (required) Actor start label. + The start node of :py:class:`~dff.script.Script`. The execution begins with it. + :param fallback_label: Actor fallback label. The label of :py:class:`~dff.script.Script`. + Dialog comes into that label if all other transitions failed, + or there was an error while executing the scenario. + :param label_priority: Default priority value for all actor :py:const:`labels <dff.script.NodeLabel3Type>` + where there is no priority. Defaults to `1.0`. + :param validation_stage: This flag sets whether the validation stage is executed in actor. + It is executed by default. Defaults to `None`. + :param condition_handler: Handler that processes a call of actor condition functions. Defaults to `None`. + :param verbose: If it is `True`, logging is used in actor. Defaults to `True`. + :param handlers: This variable is responsible for the usage of external handlers on + the certain stages of work of :py:class:`~dff.script.Actor`. + + - key :py:class:`~dff.script.ActorStage` - Stage in which the handler is called. + - value List[Callable] - The list of called handlers for each stage. Defaults to an empty `dict`. + """ + old_actor = self.actor + self.actor = Actor(script, start_label, fallback_label, label_priority, condition_handler, handlers) + errors = self.actor.validate_script(self, verbose) if validation_stage is not False else [] + if errors: + self.actor = old_actor + raise ValueError( + f"Found {len(errors)} errors: " + " ".join([f"{i}) {er}" for i, er in enumerate(errors, 1)]) + )
+ + +
+[docs] + @classmethod + def from_dict(cls, dictionary: PipelineBuilder) -> "Pipeline": + """ + Pipeline dictionary-based constructor. + Dictionary should have the fields defined in Pipeline main constructor, + it will be split and passed to it as `**kwargs`. + """ + return cls(**dictionary)
+ + +
+[docs] + async def _run_pipeline(self, request: Message, ctx_id: Optional[Hashable] = None) -> Context: + """ + Method that runs pipeline once for user request. + + :param request: (required) Any user request. + :param ctx_id: Current dialog id; if `None`, new dialog will be created. + :return: Dialog `Context`. + """ + if ctx_id is None: + ctx = Context() + elif isinstance(self.context_storage, DBContextStorage): + ctx = await self.context_storage.get_async(ctx_id, Context(id=ctx_id)) + else: + ctx = self.context_storage.get(ctx_id, Context(id=ctx_id)) + + ctx.framework_states[PIPELINE_STATE_KEY] = {} + ctx.add_request(request) + ctx = await self._services_pipeline(ctx, self) + del ctx.framework_states[PIPELINE_STATE_KEY] + + if isinstance(self.context_storage, DBContextStorage): + await self.context_storage.set_item_async(ctx_id, ctx) + else: + self.context_storage[ctx_id] = ctx + if self._clean_turn_cache: + cache_clear() + + return ctx
+ + +
+[docs] + def run(self): + """ + Method that starts a pipeline and connects to `messenger_interface`. + It passes `_run_pipeline` to `messenger_interface` as a callbacks, + so every time user request is received, `_run_pipeline` will be called. + This method can be both blocking and non-blocking. It depends on current `messenger_interface` nature. + Message interfaces that run in a loop block current thread. + """ + asyncio.run(self.messenger_interface.connect(self._run_pipeline))
+ + + def __call__(self, request: Message, ctx_id: Hashable) -> Context: + """ + Method that executes pipeline once. + Basically, it is a shortcut for `_run_pipeline`. + NB! When pipeline is executed this way, `messenger_interface` won't be initiated nor connected. + + :param request: Any user request. + :param ctx_id: Current dialog id. + :return: Dialog `Context`. + """ + return asyncio.run(self._run_pipeline(request, ctx_id)) + + @property + def script(self) -> Script: + return self.actor.script
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/pipeline/utils.html b/_modules/dff/pipeline/pipeline/utils.html new file mode 100644 index 0000000000..86c900f6af --- /dev/null +++ b/_modules/dff/pipeline/pipeline/utils.html @@ -0,0 +1,718 @@ + + + + + + + + + + dff.pipeline.pipeline.utils — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.pipeline.utils

+"""
+Utils
+-----
+The Utils module contains several service functions that are commonly used throughout the framework.
+These functions provide a variety of utility functionality.
+"""
+import collections
+from typing import Union, List
+from inspect import isfunction
+
+from ..service.service import Service
+from ..service.group import ServiceGroup
+
+
+
+[docs] +def pretty_format_component_info_dict( + service: dict, + show_extra_handlers: bool, + offset: str = "", + extra_handlers_key: str = "extra_handlers", + type_key: str = "type", + name_key: str = "name", + indent: int = 4, +) -> str: + """ + Function for dumping any pipeline components info dictionary (received from `info_dict` property) as a string. + Resulting string is formatted with YAML-like format, however it's not strict and shouldn't be parsed. + However, most preferable usage is via `pipeline.pretty_format`. + + :param service: (required) Pipeline components info dictionary. + :param show_wrappers: (required) Whether to include Wrappers or not (could be many and/or generated). + :param offset: Current level new line offset. + :param wrappers_key: Key that is mapped to Wrappers lists. + :param type_key: Key that is mapped to components type name. + :param name_key: Key that is mapped to components name. + :param indent: Current level new line offset (whitespace number). + :return: Formatted string + """ + indent = " " * indent + representation = f"{offset}{service.get(type_key, '[None]')}%s:\n" % ( + f" '{service.get(name_key, '[None]')}'" if name_key in service else "" + ) + for key, value in service.items(): + if key not in (type_key, name_key, extra_handlers_key) or (key == extra_handlers_key and show_extra_handlers): + if isinstance(value, List): + if len(value) > 0: + values = [ + pretty_format_component_info_dict(instance, show_extra_handlers, f"{indent * 2}{offset}") + for instance in value + ] + value_str = "\n%s" % "\n".join(values) + else: + value_str = "[None]" + else: + value_str = str(value) + representation += f"{offset}{indent}{key}: {value_str}\n" + return representation[:-1]
+ + + +
+[docs] +def rename_component_incrementing( + service: Union[Service, ServiceGroup], collisions: List[Union[Service, ServiceGroup]] +) -> str: + """ + Function for generating new name for a pipeline component, + that has similar name with other components in the same group. + The name is generated according to these rules: + + - If service's handler is "ACTOR", it is named `actor`. + - If service's handler is `Callable`, it is named after this `callable`. + - If it's a service group, it is named `service_group`. + - Otherwise, it is names `noname_service`. + - | After that, `_[NUMBER]` is added to the resulting name, + where `_[NUMBER]` is number of components with the same name in current service group. + + :param service: Service to be renamed. + :param collisions: Services in the same service group as service. + :return: Generated name + """ + if isinstance(service, Service) and isinstance(service.handler, str) and service.handler == "ACTOR": + base_name = "actor" + elif isinstance(service, Service) and callable(service.handler): + if isfunction(service.handler): + base_name = service.handler.__name__ + else: + base_name = service.handler.__class__.__name__ + elif isinstance(service, ServiceGroup): + base_name = "service_group" + else: + base_name = "noname_service" + + name_index = 0 + while f"{base_name}_{name_index}" in [component.name for component in collisions]: + name_index += 1 + return f"{base_name}_{name_index}"
+ + + +
+[docs] +def finalize_service_group(service_group: ServiceGroup, path: str = ".") -> bool: + """ + Function that iterates through a service group (and all its subgroups), + finalizing component's names and paths in it. + Components are renamed only if user didn't set a name for them. Their paths are also generated here. + It also searches for "ACTOR" in the group, throwing exception if no actor or multiple actors found. + + :param service_group: Service group to resolve name collisions in. + :param path: + A prefix for component paths -- path of `component` is equal to `{path}.{component.name}`. + Defaults to ".". + """ + actor = False + names_counter = collections.Counter([component.name for component in service_group.components]) + for component in service_group.components: + if component.name is None: + component.name = rename_component_incrementing(component, service_group.components) + elif names_counter[component.name] > 1: + raise Exception(f"User defined service name collision ({path})!") + component.path = f"{path}.{component.name}" + + if isinstance(component, Service) and isinstance(component.handler, str) and component.handler == "ACTOR": + actor_found = True + elif isinstance(component, ServiceGroup): + actor_found = finalize_service_group(component, f"{path}.{component.name}") + else: + actor_found = False + + if actor_found: + if not actor: + actor = actor_found + else: + raise Exception(f"More than one actor found in group ({path})!") + return actor
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/service/extra.html b/_modules/dff/pipeline/service/extra.html new file mode 100644 index 0000000000..ac7608b723 --- /dev/null +++ b/_modules/dff/pipeline/service/extra.html @@ -0,0 +1,806 @@ + + + + + + + + + + dff.pipeline.service.extra — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.service.extra

+"""
+Extra Handler
+-------------
+The Extra Handler module contains additional functionality that extends the capabilities of the system
+beyond the core functionality. Extra handlers is an input converting addition to :py:class:`.PipelineComponent`.
+For example, it is used to grep statistics from components, timing, logging, etc.
+"""
+import asyncio
+import logging
+import inspect
+from typing import Optional, List, ForwardRef
+
+from dff.script import Context
+
+from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates, wrap_sync_function_in_async
+from ..types import (
+    ServiceRuntimeInfo,
+    ExtraHandlerType,
+    ExtraHandlerBuilder,
+    ExtraHandlerFunction,
+    ExtraHandlerRuntimeInfo,
+)
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +class _ComponentExtraHandler: + """ + Class, representing an extra pipeline component handler. + A component extra handler is a set of functions, attached to pipeline component (before or after it). + Extra handlers should execute supportive tasks (like time or resources measurement, minor data transformations). + Extra handlers should NOT edit context or pipeline, use services for that purpose instead. + + :param functions: An `ExtraHandlerBuilder` object, an `_ComponentExtraHandler` instance, + a dict or a list of :py:data:`~.ExtraHandlerFunction`. + :type functions: :py:data:`~.ExtraHandlerBuilder` + :param stage: An :py:class:`~.ExtraHandlerType`, specifying whether this handler will be executed before or + after pipeline component. + :param timeout: (for asynchronous only!) Maximum component execution time (in seconds), + if it exceeds this time, it is interrupted. + :param asynchronous: Requested asynchronous property. + """ + + def __init__( + self, + functions: ExtraHandlerBuilder, + stage: ExtraHandlerType = ExtraHandlerType.UNDEFINED, + timeout: Optional[float] = None, + asynchronous: Optional[bool] = None, + ): + overridden_parameters = collect_defined_constructor_parameters_to_dict( + timeout=timeout, asynchronous=asynchronous + ) + if isinstance(functions, _ComponentExtraHandler): + self.__init__( + **_get_attrs_with_updates( + functions, + ("calculated_async_flag", "stage"), + {"requested_async_flag": "asynchronous"}, + overridden_parameters, + ) + ) + elif isinstance(functions, dict): + functions.update(overridden_parameters) + self.__init__(**functions) + elif isinstance(functions, List): + self.functions = functions + self.timeout = timeout + self.requested_async_flag = asynchronous + self.calculated_async_flag = all([asyncio.iscoroutinefunction(func) for func in self.functions]) + self.stage = stage + else: + raise Exception(f"Unknown type for {type(self).__name__} {functions}") + + @property + def asynchronous(self) -> bool: + """ + Property, that indicates, whether this component extra handler is synchronous or asynchronous. + It is calculated according to the following rules: + + - | If component **can** be asynchronous and :py:attr:`~requested_async_flag` is set, + it returns :py:attr:`~requested_async_flag`. + - | If component **can** be asynchronous and :py:attr:`~requested_async_flag` isn't set, + it returns `True`. + - | If component **can't** be asynchronous and :py:attr:`~requested_async_flag` is `False` or not set, + it returns `False`. + - | If component **can't** be asynchronous and :py:attr:`~requested_async_flag` is `True`, + an Exception is thrown in constructor. + + """ + return self.calculated_async_flag if self.requested_async_flag is None else self.requested_async_flag + +
+[docs] + async def _run_function( + self, func: ExtraHandlerFunction, ctx: Context, pipeline: Pipeline, component_info: ServiceRuntimeInfo + ): + handler_params = len(inspect.signature(func).parameters) + if handler_params == 1: + await wrap_sync_function_in_async(func, ctx) + elif handler_params == 2: + await wrap_sync_function_in_async(func, ctx, pipeline) + elif handler_params == 3: + extra_handler_runtime_info = ExtraHandlerRuntimeInfo(func=func, stage=self.stage, component=component_info) + await wrap_sync_function_in_async(func, ctx, pipeline, extra_handler_runtime_info) + else: + raise Exception( + f"Too many parameters required for component {component_info.name} {self.stage}" + f" wrapper handler '{func.__name__}': {handler_params}!" + )
+ + +
+[docs] + async def _run(self, ctx: Context, pipeline: Pipeline, component_info: ServiceRuntimeInfo): + """ + Method for executing one of the wrapper functions (before or after). + If the function is not set, nothing happens. + + :param stage: current `WrapperStage` (before or after). + :param ctx: current dialog context. + :param pipeline: the current pipeline. + :param component_info: associated component's info dictionary. + :return: `None` + """ + + if self.asynchronous: + futures = [self._run_function(func, ctx, pipeline, component_info) for func in self.functions] + for func, future in zip(self.functions, asyncio.as_completed(futures)): + try: + await future + except asyncio.TimeoutError: + logger.warning(f"Component {component_info.name} {self.stage} wrapper '{func.__name__}' timed out!") + + else: + for func in self.functions: + await self._run_function(func, ctx, pipeline, component_info)
+ + + async def __call__(self, ctx: Context, pipeline: Pipeline, component_info: ServiceRuntimeInfo): + """ + A method for calling pipeline components. + It sets up timeout if this component is asynchronous and executes it using `_run` method. + + :param ctx: (required) Current dialog `Context`. + :param pipeline: This `Pipeline`. + :return: `Context` if this is a synchronous service or + `Awaitable` if this is an asynchronous component or `None`. + """ + if self.asynchronous: + task = asyncio.create_task(self._run(ctx, pipeline, component_info)) + return await asyncio.wait_for(task, timeout=self.timeout) + else: + return await self._run(ctx, pipeline, component_info) + + @property + def info_dict(self) -> dict: + """ + Property for retrieving info dictionary about this wrapper. + + :return: Info dict, containing its fields as well as its type. + All not set fields there are replaced with `None`. + """ + return { + "type": type(self).__name__, + "timeout": self.timeout, + "asynchronous": self.asynchronous, + "functions": [func.__name__ for func in self.functions], + }
+ + + +
+[docs] +class BeforeHandler(_ComponentExtraHandler): + """ + A handler for extra functions that are executed before the component's main function. + + :param functions: A callable or a list of callables that will be executed + before the component's main function. + :type functions: ExtraHandlerBuilder + :param timeout: Optional timeout for the execution of the extra functions, in + seconds. + :param asynchronous: Optional flag that indicates whether the extra functions + should be executed asynchronously. The default value of the flag is True + if all the functions in this handler are asynchronous. + """ + + def __init__( + self, + functions: ExtraHandlerBuilder, + timeout: Optional[int] = None, + asynchronous: Optional[bool] = None, + ): + super().__init__(functions, ExtraHandlerType.BEFORE, timeout, asynchronous)
+ + + +
+[docs] +class AfterHandler(_ComponentExtraHandler): + """ + A handler for extra functions that are executed after the component's main function. + + :param functions: A callable or a list of callables that will be executed + after the component's main function. + :type functions: ExtraHandlerBuilder + :param timeout: Optional timeout for the execution of the extra functions, in + seconds. + :param asynchronous: Optional flag that indicates whether the extra functions + should be executed asynchronously. The default value of the flag is True + if all the functions in this handler are asynchronous. + """ + + def __init__( + self, + functions: ExtraHandlerBuilder, + timeout: Optional[int] = None, + asynchronous: Optional[bool] = None, + ): + super().__init__(functions, ExtraHandlerType.AFTER, timeout, asynchronous)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/service/group.html b/_modules/dff/pipeline/service/group.html new file mode 100644 index 0000000000..d2f51c6c9b --- /dev/null +++ b/_modules/dff/pipeline/service/group.html @@ -0,0 +1,850 @@ + + + + + + + + + + dff.pipeline.service.group — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.service.group

+"""
+Service Group
+-------------
+The Service Group module contains the
+:py:class:`~.ServiceGroup` class, which is used to represent a group of related services.
+This class provides a way to organize and manage multiple services as a single unit,
+allowing for easier management and organization of the services within the pipeline.
+The :py:class:`~.ServiceGroup` serves the important function of grouping services to work together in parallel.
+"""
+import asyncio
+import logging
+from typing import Optional, List, Union, Awaitable, ForwardRef
+
+from dff.script import Context
+
+from .utils import collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates
+from ..pipeline.component import PipelineComponent
+from ..types import (
+    StartConditionCheckerFunction,
+    ComponentExecutionState,
+    ServiceGroupBuilder,
+    GlobalExtraHandlerType,
+    ExtraHandlerConditionFunction,
+    ExtraHandlerFunction,
+    ExtraHandlerBuilder,
+    ExtraHandlerType,
+)
+from .service import Service
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +class ServiceGroup(PipelineComponent): + """ + A service group class. + Service group can be included into pipeline as an object or a pipeline component list. + Service group can be synchronous or asynchronous. + Components in synchronous groups are executed consequently (no matter is they are synchronous or asynchronous). + Components in asynchronous groups are executed simultaneously. + Group can be asynchronous only if all components in it are asynchronous. + Group containing actor can be synchronous only. + + :param components: A `ServiceGroupBuilder` object, that will be added to the group. + :type components: :py:data:`~.ServiceGroupBuilder` + :param before_handler: List of `ExtraHandlerBuilder` to add to the group. + :type before_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param after_handler: List of `ExtraHandlerBuilder` to add to the group. + :type after_handler: Optional[:py:data:`~.ExtraHandlerBuilder`] + :param timeout: Timeout to add to the group. + :param asynchronous: Requested asynchronous property. + :param start_condition: :py:data:`~.StartConditionCheckerFunction` that is invoked before each group execution; + group is executed only if it returns `True`. + :param name: Requested group name. + """ + + def __init__( + self, + components: ServiceGroupBuilder, + before_handler: Optional[ExtraHandlerBuilder] = None, + after_handler: Optional[ExtraHandlerBuilder] = None, + timeout: Optional[float] = None, + asynchronous: Optional[bool] = None, + start_condition: Optional[StartConditionCheckerFunction] = None, + name: Optional[str] = None, + ): + overridden_parameters = collect_defined_constructor_parameters_to_dict( + before_handler=before_handler, + after_handler=after_handler, + timeout=timeout, + asynchronous=asynchronous, + start_condition=start_condition, + name=name, + ) + if isinstance(components, ServiceGroup): + self.__init__( + **_get_attrs_with_updates( + components, + ( + "calculated_async_flag", + "path", + ), + {"requested_async_flag": "asynchronous"}, + overridden_parameters, + ) + ) + elif isinstance(components, dict): + components.update(overridden_parameters) + self.__init__(**components) + elif isinstance(components, List): + self.components = self._create_components(components) + calc_async = all([service.asynchronous for service in self.components]) + super(ServiceGroup, self).__init__( + before_handler, after_handler, timeout, asynchronous, calc_async, start_condition, name + ) + else: + raise Exception(f"Unknown type for ServiceGroup {components}") + +
+[docs] + async def _run_services_group(self, ctx: Context, pipeline: Pipeline) -> Context: + """ + Method for running this service group. + It doesn't include wrappers execution, start condition checking or error handling - pure execution only. + Executes components inside the group based on its `asynchronous` property. + Collects information about their execution state - group is finished successfully + only if all components in it finished successfully. + + :param ctx: Current dialog context. + :param pipeline: The current pipeline. + :return: Current dialog context. + """ + self._set_state(ctx, ComponentExecutionState.RUNNING) + + if self.asynchronous: + service_futures = [service(ctx, pipeline) for service in self.components] + for service, future in zip(self.components, await asyncio.gather(*service_futures, return_exceptions=True)): + service_result = future + if service.asynchronous and isinstance(service_result, Awaitable): + await service_result + elif isinstance(service_result, asyncio.TimeoutError): + logger.warning(f"{type(service).__name__} '{service.name}' timed out!") + + else: + for service in self.components: + service_result = await service(ctx, pipeline) + if not service.asynchronous and isinstance(service_result, Context): + ctx = service_result + elif service.asynchronous and isinstance(service_result, Awaitable): + await service_result + + failed = any([service.get_state(ctx) == ComponentExecutionState.FAILED for service in self.components]) + self._set_state(ctx, ComponentExecutionState.FAILED if failed else ComponentExecutionState.FINISHED) + return ctx
+ + +
+[docs] + async def _run( + self, + ctx: Context, + pipeline: Pipeline = None, + ) -> Optional[Context]: + """ + Method for handling this group execution. + Executes before and after execution wrappers, checks start condition and catches runtime exceptions. + + :param ctx: Current dialog context. + :param pipeline: The current pipeline. + :return: Current dialog context if synchronous, else `None`. + """ + await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) + + try: + if self.start_condition(ctx, pipeline): + ctx = await self._run_services_group(ctx, pipeline) + else: + self._set_state(ctx, ComponentExecutionState.NOT_RUN) + + except Exception as e: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"ServiceGroup '{self.name}' execution failed!\n{e}") + + await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) + return ctx if not self.asynchronous else None
+ + +
+[docs] + def log_optimization_warnings(self): + """ + Method for logging service group optimization warnings for all this groups inner components. + (NOT this group itself!). + Warnings are basically messages, + that indicate service group inefficiency or explicitly defined parameters mismatch. + These are cases for warnings issuing: + + - Service can be asynchronous, however is marked synchronous explicitly. + - Service is not asynchronous, however has a timeout defined. + - Group is not marked synchronous explicitly and contains both synchronous and asynchronous components. + + :return: `None` + """ + for service in self.components: + if isinstance(service, Service): + if ( + service.calculated_async_flag + and service.requested_async_flag is not None + and not service.requested_async_flag + ): + logger.warning(f"Service '{service.name}' could be asynchronous!") + if not service.asynchronous and service.timeout is not None: + logger.warning(f"Timeout can not be applied for Service '{service.name}': it's not asynchronous!") + else: + if not service.calculated_async_flag: + if service.requested_async_flag is None and any( + [sub_service.asynchronous for sub_service in service.components] + ): + logger.warning( + f"ServiceGroup '{service.name}' contains both sync and async services, " + "it should be split or marked as synchronous explicitly!", + ) + service.log_optimization_warnings()
+ + +
+[docs] + def add_extra_handler( + self, + global_extra_handler_type: GlobalExtraHandlerType, + extra_handler: ExtraHandlerFunction, + condition: ExtraHandlerConditionFunction = lambda _: True, + ): + """ + Method for adding a global wrapper to this group. + Adds wrapper to itself and propagates it to all inner components. + Uses a special condition function to determine whether to add wrapper to any particular inner component or not. + Condition checks components path to be in whitelist (if defined) and not to be in blacklist (if defined). + + :param global_extra_handler_type: A type of wrapper to add. + :param extra_handler: A `WrapperFunction` to add as a wrapper. + :type extra_handler: :py:data:`~.ExtraHandlerFunction` + :param condition: A condition function. + :return: `None` + """ + super().add_extra_handler(global_extra_handler_type, extra_handler) + for service in self.components: + if not condition(service.path): + continue + if isinstance(service, Service): + service.add_extra_handler(global_extra_handler_type, extra_handler) + else: + service.add_extra_handler(global_extra_handler_type, extra_handler, condition)
+ + + @property + def info_dict(self) -> dict: + """ + See `Component.info_dict` property. + Adds `services` key to base info dictionary. + """ + representation = super(ServiceGroup, self).info_dict + representation.update({"services": [service.info_dict for service in self.components]}) + return representation + +
+[docs] + @staticmethod + def _create_components(services: ServiceGroupBuilder) -> List[Union[Service, "ServiceGroup"]]: + """ + Utility method, used to create inner components, judging by their nature. + Services are created from services and dictionaries. + ServiceGroups are created from service groups and lists. + + :param services: ServiceGroupBuilder object (a `ServiceGroup` instance or a list). + :type services: :py:data:`~.ServiceGroupBuilder` + :return: List of services and service groups. + """ + handled_services: List[Union[Service, "ServiceGroup"]] = [] + for service in services: + if isinstance(service, List) or isinstance(service, ServiceGroup): + handled_services.append(ServiceGroup(service)) + else: + handled_services.append(Service(service)) + return handled_services
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/service/service.html b/_modules/dff/pipeline/service/service.html new file mode 100644 index 0000000000..042a308613 --- /dev/null +++ b/_modules/dff/pipeline/service/service.html @@ -0,0 +1,825 @@ + + + + + + + + + + dff.pipeline.service.service — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.service.service

+"""
+Service
+-------
+The Service module contains the :py:class:`.Service` class,
+which can be included into pipeline as object or a dictionary.
+Pipeline consists of services and service groups.
+Service group can be synchronous or asynchronous.
+Service is an atomic part of a pipeline.
+Service can be asynchronous only if its handler is a coroutine.
+Actor wrapping service can be synchronous only.
+"""
+# TODO: change last sentence, when actor will be asynchronous
+
+import logging
+import asyncio
+import inspect
+from typing import Optional, ForwardRef
+
+from dff.script import Context
+
+from .utils import wrap_sync_function_in_async, collect_defined_constructor_parameters_to_dict, _get_attrs_with_updates
+from ..types import (
+    ServiceBuilder,
+    StartConditionCheckerFunction,
+    ComponentExecutionState,
+    ExtraHandlerBuilder,
+    ExtraHandlerType,
+)
+from ..pipeline.component import PipelineComponent
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +class Service(PipelineComponent): + """ + This class represents a service. + Service can be included into pipeline as object or a dictionary. + Service group can be synchronous or asynchronous. + Service can be asynchronous only if its handler is a coroutine. + Actor wrapping service can be synchronous only. + + :param handler: A service function or an actor. + :type handler: :py:data:`~.ServiceBuilder` + :param wrappers: List of Wrappers to add to the service. + :param timeout: Timeout to add to the group. + :param asynchronous: Requested asynchronous property. + :param start_condition: StartConditionCheckerFunction that is invoked before each service execution; + service is executed only if it returns `True`. + :type start_condition: Optional[:py:data:`~.StartConditionCheckerFunction`] + :param name: Requested service name. + """ + + def __init__( + self, + handler: ServiceBuilder, + before_handler: Optional[ExtraHandlerBuilder] = None, + after_handler: Optional[ExtraHandlerBuilder] = None, + timeout: Optional[float] = None, + asynchronous: Optional[bool] = None, + start_condition: Optional[StartConditionCheckerFunction] = None, + name: Optional[str] = None, + ): + overridden_parameters = collect_defined_constructor_parameters_to_dict( + before_handler=before_handler, + after_handler=after_handler, + timeout=timeout, + asynchronous=asynchronous, + start_condition=start_condition, + name=name, + ) + if isinstance(handler, dict): + handler.update(overridden_parameters) + self.__init__(**handler) + elif isinstance(handler, Service): + self.__init__( + **_get_attrs_with_updates( + handler, + ( + "calculated_async_flag", + "path", + ), + {"requested_async_flag": "asynchronous"}, + overridden_parameters, + ) + ) + elif callable(handler) or isinstance(handler, str) and handler == "ACTOR": + self.handler = handler + super(Service, self).__init__( + before_handler, + after_handler, + timeout, + asynchronous, + asyncio.iscoroutinefunction(handler), + start_condition, + name, + ) + else: + raise Exception(f"Unknown type of service handler: {handler}") + +
+[docs] + async def _run_handler(self, ctx: Context, pipeline: Pipeline): + """ + Method for service `handler` execution. + Handler has three possible signatures, so this method picks the right one to invoke. + These possible signatures are: + + - (ctx: Context) - accepts current dialog context only. + - (ctx: Context, pipeline: Pipeline) - accepts context and current pipeline. + - | (ctx: Context, pipeline: Pipeline, info: ServiceRuntimeInfo) - accepts context, + pipeline and service runtime info dictionary. + + :param ctx: Current dialog context. + :param pipeline: The current pipeline. + :return: `None` + """ + handler_params = len(inspect.signature(self.handler).parameters) + if handler_params == 1: + await wrap_sync_function_in_async(self.handler, ctx) + elif handler_params == 2: + await wrap_sync_function_in_async(self.handler, ctx, pipeline) + elif handler_params == 3: + await wrap_sync_function_in_async(self.handler, ctx, pipeline, self._get_runtime_info(ctx)) + else: + raise Exception(f"Too many parameters required for service '{self.name}' handler: {handler_params}!")
+ + +
+[docs] + def _run_as_actor(self, ctx: Context, pipeline: Pipeline): + """ + Method for running this service if its handler is an `Actor`. + Catches runtime exceptions. + + :param ctx: Current dialog context. + :return: Context, mutated by actor. + """ + try: + ctx = pipeline.actor(pipeline, ctx) + self._set_state(ctx, ComponentExecutionState.FINISHED) + except Exception as exc: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"Actor '{self.name}' execution failed!\n{exc}") + return ctx
+ + +
+[docs] + async def _run_as_service(self, ctx: Context, pipeline: Pipeline): + """ + Method for running this service if its handler is not an Actor. + Checks start condition and catches runtime exceptions. + + :param ctx: Current dialog context. + :param pipeline: Current pipeline. + :return: `None` + """ + try: + if self.start_condition(ctx, pipeline): + self._set_state(ctx, ComponentExecutionState.RUNNING) + await self._run_handler(ctx, pipeline) + self._set_state(ctx, ComponentExecutionState.FINISHED) + else: + self._set_state(ctx, ComponentExecutionState.NOT_RUN) + except Exception as e: + self._set_state(ctx, ComponentExecutionState.FAILED) + logger.error(f"Service '{self.name}' execution failed!\n{e}")
+ + +
+[docs] + async def _run(self, ctx: Context, pipeline: Optional[Pipeline] = None) -> Optional[Context]: + """ + Method for handling this service execution. + Executes before and after execution wrappers, launches `_run_as_actor` or `_run_as_service` method. + + :param ctx: (required) Current dialog context. + :param pipeline: the current pipeline. + :return: `Context` if this service's handler is an `Actor` else `None`. + """ + await self.run_extra_handler(ExtraHandlerType.BEFORE, ctx, pipeline) + + if isinstance(self.handler, str) and self.handler == "ACTOR": + ctx = self._run_as_actor(ctx, pipeline) + else: + await self._run_as_service(ctx, pipeline) + + await self.run_extra_handler(ExtraHandlerType.AFTER, ctx, pipeline) + + if isinstance(self.handler, str) and self.handler == "ACTOR": + return ctx + return None
+ + + @property + def info_dict(self) -> dict: + """ + See `Component.info_dict` property. + Adds `handler` key to base info dictionary. + """ + representation = super(Service, self).info_dict + if isinstance(self.handler, str) and self.handler == "ACTOR": + service_representation = "Instance of Actor" + elif callable(self.handler): + service_representation = f"Callable '{self.handler.__name__}'" + else: + service_representation = "[Unknown]" + representation.update({"handler": service_representation}) + return representation
+ + + +
+[docs] +def to_service( + before_handler: Optional[ExtraHandlerBuilder] = None, + after_handler: Optional[ExtraHandlerBuilder] = None, + timeout: Optional[int] = None, + asynchronous: Optional[bool] = None, + start_condition: Optional[StartConditionCheckerFunction] = None, + name: Optional[str] = None, +): + """ + Function for decorating a function as a Service. + Returns a Service, constructed from this function (taken as a handler). + All arguments are passed directly to `Service` constructor. + """ + + def inner(handler: ServiceBuilder) -> Service: + return Service( + handler=handler, + before_handler=before_handler, + after_handler=after_handler, + timeout=timeout, + asynchronous=asynchronous, + start_condition=start_condition, + name=name, + ) + + return inner
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/service/utils.html b/_modules/dff/pipeline/service/utils.html new file mode 100644 index 0000000000..0b396150d9 --- /dev/null +++ b/_modules/dff/pipeline/service/utils.html @@ -0,0 +1,658 @@ + + + + + + + + + + dff.pipeline.service.utils — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.service.utils

+"""
+Utility Functions
+-----------------
+The Utility Functions module contains several utility functions that are commonly used throughout the DFF.
+These functions provide a variety of utility functionality.
+"""
+import asyncio
+from typing import Callable, Any, Optional, Tuple, Mapping
+
+
+
+[docs] +async def wrap_sync_function_in_async(func: Callable, *args, **kwargs) -> Any: + """ + Utility function, that wraps both functions and coroutines in coroutines. + Invokes `func` if it is just a callable and awaits, if this is a coroutine. + + :param func: Callable to wrap. + :param \\*args: Function args. + :param \\**kwargs: Function kwargs. + :return: What function returns. + """ + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + else: + return func(*args, **kwargs)
+ + + +
+[docs] +def _get_attrs_with_updates( + obj: object, + drop_attrs: Optional[Tuple[str, ...]] = None, + replace_attrs: Optional[Mapping[str, str]] = None, + add_attrs: Optional[Mapping[str, Any]] = None, +) -> dict: + """ + Advanced customizable version of built-in `__dict__` property. + Sometimes during Pipeline construction `Services` (or `ServiceGroups`) should be rebuilt, + e.g. in case of some fields overriding. + This method can be customized to return a dict, + that can be spread (** operator) and passed to Service or ServiceGroup constructor. + Base dict is formed via `vars` built-in function. All "private" or "dunder" fields are omitted. + + :param drop_attrs: A tuple of key names that should be removed from the resulting dict. + :param replace_attrs: A mapping that should be replaced in the resulting dict. + :param add_attrs: A mapping that should be added to the resulting dict. + :return: Resulting dict. + """ + drop_attrs = () if drop_attrs is None else drop_attrs + replace_attrs = {} if replace_attrs is None else dict(replace_attrs) + add_attrs = {} if add_attrs is None else dict(add_attrs) + result = {} + for attribute in vars(obj): + if not attribute.startswith("__") and attribute not in drop_attrs: + if attribute in replace_attrs: + result[replace_attrs[attribute]] = getattr(obj, attribute) + else: + result[attribute] = getattr(obj, attribute) + result.update(add_attrs) + return result
+ + + +
+[docs] +def collect_defined_constructor_parameters_to_dict(**kwargs: Any): + """ + Function, that creates dict from non-`None` constructor parameters of pipeline component. + It is used in overriding component parameters, + when service handler or service group service is instance of Service or ServiceGroup (or dict). + It accepts same named parameters as component constructor. + + :return: Dict, containing key-value pairs of these parameters, that are not `None`. + """ + return dict([(key, value) for key, value in kwargs.items() if value is not None])
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/pipeline/types.html b/_modules/dff/pipeline/types.html new file mode 100644 index 0000000000..e7da66e18e --- /dev/null +++ b/_modules/dff/pipeline/types.html @@ -0,0 +1,845 @@ + + + + + + + + + + dff.pipeline.types — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.pipeline.types

+"""
+Types
+-----
+The Types module contains several classes and special types that are used throughout the `DFF Pipeline`.
+The classes and special types in this module can include data models,
+data structures, and other types that are defined for type hinting.
+"""
+from abc import ABC
+from enum import unique, Enum
+from typing import Callable, Union, Awaitable, Dict, List, Optional, NewType, Iterable, Any
+
+from dff.context_storages import DBContextStorage
+from dff.script import Context, ActorStage, NodeLabel2Type, Script
+from typing_extensions import NotRequired, TypedDict, TypeAlias
+from pydantic import BaseModel
+
+
+_ForwardPipeline = NewType("Pipeline", Any)
+_ForwardPipelineComponent = NewType("PipelineComponent", Any)
+_ForwardService = NewType("Service", _ForwardPipelineComponent)
+_ForwardServiceBuilder = NewType("ServiceBuilder", Any)
+_ForwardServiceGroup = NewType("ServiceGroup", _ForwardPipelineComponent)
+_ForwardComponentExtraHandler = NewType("_ComponentExtraHandler", Any)
+_ForwardProvider = NewType("ABCProvider", ABC)
+_ForwardExtraHandlerRuntimeInfo = NewType("ExtraHandlerRuntimeInfo", Any)
+
+
+
+[docs] +@unique +class ComponentExecutionState(str, Enum): + """ + Enum, representing pipeline component execution state. + These states are stored in `ctx.framework_keys[PIPELINE_STATE_KEY]`, + that should always be requested with `NOT_RUN` being default fallback. + Following states are supported: + + - NOT_RUN: component has not been executed yet (the default one), + - RUNNING: component is currently being executed, + - FINISHED: component executed successfully, + - FAILED: component execution failed. + """ + + NOT_RUN = "NOT_RUN" + RUNNING = "RUNNING" + FINISHED = "FINISHED" + FAILED = "FAILED"
+ + + +
+[docs] +@unique +class GlobalExtraHandlerType(str, Enum): + """ + Enum, representing types of global wrappers, that can be set applied for a pipeline. + The following types are supported: + + - BEFORE_ALL: function called before each pipeline call, + - BEFORE: function called before each component, + - AFTER: function called after each component, + - AFTER_ALL: function called after each pipeline call. + """ + + BEFORE_ALL = "BEFORE_ALL" + BEFORE = "BEFORE" + AFTER = "AFTER" + AFTER_ALL = "AFTER_ALL"
+ + + +
+[docs] +@unique +class ExtraHandlerType(str, Enum): + """ + Enum, representing wrapper execution stage: before or after the wrapped function. + The following types are supported: + + - UNDEFINED: wrapper function with undetermined execution stage, + - BEFORE: wrapper function called before component, + - AFTER: wrapper function called after component. + """ + + UNDEFINED = "UNDEFINED" + BEFORE = "BEFORE" + AFTER = "AFTER"
+ + + +PIPELINE_STATE_KEY = "PIPELINE" +""" +PIPELINE: storage for services and groups execution status. +Should be used in `ctx.framework_keys[PIPELINE_STATE_KEY]`. +""" + + +StartConditionCheckerFunction: TypeAlias = Callable[[Context, _ForwardPipeline], bool] +""" +A function type for components `start_conditions`. +Accepts context and pipeline, returns boolean (whether service can be launched). +""" + + +StartConditionCheckerAggregationFunction: TypeAlias = Callable[[Iterable[bool]], bool] +""" +A function type for creating aggregation `start_conditions` for components. +Accepts list of functions (other start_conditions to aggregate), returns boolean (whether service can be launched). +""" + + +ExtraHandlerConditionFunction: TypeAlias = Callable[[str], bool] +""" +A function type used during global wrappers initialization to determine +whether wrapper should be applied to component with given path or not. +Checks components path to be in whitelist (if defined) and not to be in blacklist (if defined). +Accepts str (component path), returns boolean (whether wrapper should be applied). +""" + + +
+[docs] +class ServiceRuntimeInfo(BaseModel): + """ + Type of object, that is passed to components in runtime. + Contains current component info (`name`, `path`, `timeout`, `asynchronous`). + Also contains `execution_state` - a dictionary, + containing execution states of other components mapped to their paths. + """ + + name: str + path: str + timeout: Optional[float] + asynchronous: bool + execution_state: Dict[str, ComponentExecutionState]
+ + + +ExtraHandlerFunction: TypeAlias = Union[ + Callable[[Context], Any], + Callable[[Context, _ForwardPipeline], Any], + Callable[[Context, _ForwardPipeline, _ForwardExtraHandlerRuntimeInfo], Any], +] +""" +A function type for creating wrappers (before and after functions). +Can accept current dialog context, pipeline, and current wrapper info. +""" + + +
+[docs] +class ExtraHandlerRuntimeInfo(BaseModel): + func: ExtraHandlerFunction + stage: ExtraHandlerType + component: ServiceRuntimeInfo
+ + + +""" +Type of object, that is passed to wrappers in runtime. +Contains current wrapper info (`name`, `stage`). +Also contains `component` - runtime info of the component this wrapper is attached to. +""" + + +ServiceFunction: TypeAlias = Union[ + Callable[[Context], None], + Callable[[Context], Awaitable[None]], + Callable[[Context, _ForwardPipeline], None], + Callable[[Context, _ForwardPipeline], Awaitable[None]], + Callable[[Context, _ForwardPipeline, ServiceRuntimeInfo], None], + Callable[[Context, _ForwardPipeline, ServiceRuntimeInfo], Awaitable[None]], +] +""" +A function type for creating service handlers. +Can accept current dialog context, pipeline, and current service info. +Can be both synchronous and asynchronous. +""" + + +ExtraHandlerBuilder: TypeAlias = Union[ + _ForwardComponentExtraHandler, + TypedDict( + "WrapperDict", + { + "timeout": NotRequired[Optional[float]], + "asynchronous": NotRequired[bool], + "functions": List[ExtraHandlerFunction], + }, + ), + List[ExtraHandlerFunction], +] +""" +A type, representing anything that can be transformed to ExtraHandlers. +It can be: + +- _ForwardComponentExtraHandler object +- Dictionary, containing keys `timeout`, `asynchronous`, `functions` +""" + + +ServiceBuilder: TypeAlias = Union[ + ServiceFunction, + _ForwardService, + str, + TypedDict( + "ServiceDict", + { + "handler": _ForwardServiceBuilder, + "before_handler": NotRequired[Optional[ExtraHandlerBuilder]], + "after_handler": NotRequired[Optional[ExtraHandlerBuilder]], + "timeout": NotRequired[Optional[float]], + "asynchronous": NotRequired[bool], + "start_condition": NotRequired[StartConditionCheckerFunction], + "name": Optional[str], + }, + ), +] +""" +A type, representing anything that can be transformed to service. +It can be: + +- ServiceFunction (will become handler) +- Service object (will be spread and recreated) +- String 'ACTOR' - the pipeline Actor will be placed there +- Dictionary, containing keys that are present in Service constructor parameters +""" + + +ServiceGroupBuilder: TypeAlias = Union[ + List[Union[ServiceBuilder, List[ServiceBuilder], _ForwardServiceGroup]], + _ForwardServiceGroup, +] +""" +A type, representing anything that can be transformed to service group. +It can be: + +- List of `ServiceBuilders`, `ServiceGroup` objects and lists (recursive) +- `ServiceGroup` object (will be spread and recreated) +""" + + +PipelineBuilder: TypeAlias = TypedDict( + "PipelineBuilder", + { + "messenger_interface": NotRequired[Optional[_ForwardProvider]], + "context_storage": NotRequired[Optional[Union[DBContextStorage, Dict]]], + "components": ServiceGroupBuilder, + "before_handler": NotRequired[Optional[ExtraHandlerBuilder]], + "after_handler": NotRequired[Optional[ExtraHandlerBuilder]], + "optimization_warnings": NotRequired[bool], + "script": Union[Script, Dict], + "start_label": NodeLabel2Type, + "fallback_label": NotRequired[Optional[NodeLabel2Type]], + "label_priority": NotRequired[float], + "validation_stage": NotRequired[Optional[bool]], + "condition_handler": NotRequired[Optional[Callable]], + "verbose": NotRequired[bool], + "handlers": NotRequired[Optional[Dict[ActorStage, List[Callable]]]], + }, +) +""" +A type, representing anything that can be transformed to pipeline. +It can be Dictionary, containing keys that are present in Pipeline constructor parameters. +""" +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/conditions/std_conditions.html b/_modules/dff/script/conditions/std_conditions.html new file mode 100644 index 0000000000..488f82aa39 --- /dev/null +++ b/_modules/dff/script/conditions/std_conditions.html @@ -0,0 +1,839 @@ + + + + + + + + + + dff.script.conditions.std_conditions — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.conditions.std_conditions

+"""
+Conditions
+----------
+Conditions are one of the most important components of the dialog graph.
+They determine the possibility of transition from one node of the graph to another.
+The conditions are used to specify when a particular transition should occur, based on certain criteria.
+This module contains a standard set of scripting conditions that can be used to control the flow of a conversation.
+These conditions can be used to check the current context, the user's input,
+or other factors that may affect the conversation flow.
+"""
+from typing import Callable, Pattern, Union, Any, List, Optional
+import logging
+import re
+
+from pydantic import validate_call
+
+from dff.pipeline import Pipeline
+from dff.script import NodeLabel2Type, Context, Message
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +@validate_call +def exact_match(match: Message, skip_none: bool = True) -> Callable[..., bool]: + """ + Return function handler. This handler returns `True` only if the last user phrase + is the same Message as the :py:const:`match`. + If :py:const:`skip_none` the handler will not compare `None` fields of :py:const:`match`. + + :param match: A Message variable to compare user request with. + :param skip_none: Whether fields should be compared if they are `None` in :py:const:`match`. + """ + + def exact_match_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + request = ctx.last_request + if request is None: + return False + for field in match.model_fields: + match_value = match.__getattribute__(field) + if skip_none and match_value is None: + continue + if field in request.model_fields.keys(): + if request.__getattribute__(field) != match.__getattribute__(field): + return False + else: + return False + return True + + return exact_match_condition_handler
+ + + +
+[docs] +@validate_call +def regexp( + pattern: Union[str, Pattern], flags: Union[int, re.RegexFlag] = 0 +) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler returns `True` only if the last user phrase contains + :py:const:`pattern <Union[str, Pattern]>` with :py:const:`flags <Union[int, re.RegexFlag]>`. + + :param pattern: The `RegExp` pattern. + :param flags: Flags for this pattern. Defaults to 0. + """ + pattern = re.compile(pattern, flags) + + def regexp_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + request = ctx.last_request + if isinstance(request, Message): + if request.text is None: + return False + return bool(pattern.search(request.text)) + else: + logger.error(f"request has to be str type, but got request={request}") + return False + + return regexp_condition_handler
+ + + +
+[docs] +@validate_call +def check_cond_seq(cond_seq: list): + """ + Check if the list consists only of Callables. + + :param cond_seq: List of conditions to check. + """ + for cond in cond_seq: + if not callable(cond): + raise TypeError(f"{cond_seq} has to consist of callable objects")
+ + + +_any = any +""" +_any is an alias for any. +""" +_all = all +""" +_all is an alias for all. +""" + + +
+[docs] +@validate_call +def aggregate(cond_seq: list, aggregate_func: Callable = _any) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Aggregate multiple functions into one by using aggregating function. + + :param cond_seq: List of conditions to check. + :param aggregate_func: Function to aggregate conditions. Defaults to :py:func:`_any`. + """ + check_cond_seq(cond_seq) + + def aggregate_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + try: + return bool(aggregate_func([cond(ctx, pipeline, *args, **kwargs) for cond in cond_seq])) + except Exception as exc: + logger.error(f"Exception {exc} for {cond_seq}, {aggregate_func} and {ctx.last_request}", exc_info=exc) + return False + + return aggregate_condition_handler
+ + + +
+[docs] +@validate_call +def any(cond_seq: list) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler returns `True` + if any function from the list is `True`. + + :param cond_seq: List of conditions to check. + """ + _agg = aggregate(cond_seq, _any) + + def any_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return _agg(ctx, pipeline, *args, **kwargs) + + return any_condition_handler
+ + + +
+[docs] +@validate_call +def all(cond_seq: list) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler returns `True` only + if all functions from the list are `True`. + + :param cond_seq: List of conditions to check. + """ + _agg = aggregate(cond_seq, _all) + + def all_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return _agg(ctx, pipeline, *args, **kwargs) + + return all_condition_handler
+ + + +
+[docs] +@validate_call +def negation(condition: Callable) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler returns negation of the :py:func:`~condition`: `False` + if :py:func:`~condition` holds `True` and returns `True` otherwise. + + :param condition: Any :py:func:`~condition`. + """ + + def negation_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return not condition(ctx, pipeline, *args, **kwargs) + + return negation_condition_handler
+ + + +
+[docs] +@validate_call +def has_last_labels( + flow_labels: Optional[List[str]] = None, + labels: Optional[List[NodeLabel2Type]] = None, + last_n_indices: int = 1, +) -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return condition handler. This handler returns `True` if any label from + last :py:const:`last_n_indices` context labels is in + the :py:const:`flow_labels` list or in + the :py:const:`~dff.script.NodeLabel2Type` list. + + :param flow_labels: List of labels to check. Every label has type `str`. Empty if not set. + :param labels: List of labels corresponding to the nodes. Empty if not set. + :param last_n_indices: Number of last utterances to check. + """ + flow_labels = [] if flow_labels is None else flow_labels + labels = [] if labels is None else labels + + def has_last_labels_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + label = list(ctx.labels.values())[-last_n_indices:] + for label in list(ctx.labels.values())[-last_n_indices:]: + label = label if label else (None, None) + if label[0] in flow_labels or label in labels: + return True + return False + + return has_last_labels_condition_handler
+ + + +
+[docs] +@validate_call +def true() -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler always returns `True`. + """ + + def true_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return True + + return true_handler
+ + + +
+[docs] +@validate_call +def false() -> Callable[[Context, Pipeline, Any, Any], bool]: + """ + Return function handler. This handler always returns `False`. + """ + + def false_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return False + + return false_handler
+ + + +# aliases +agg = aggregate +""" +:py:func:`~agg` is an alias for :py:func:`~aggregate`. +""" +neg = negation +""" +:py:func:`~neg` is an alias for :py:func:`~negation`. +""" +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/context.html b/_modules/dff/script/core/context.html new file mode 100644 index 0000000000..b877a6e04e --- /dev/null +++ b/_modules/dff/script/core/context.html @@ -0,0 +1,907 @@ + + + + + + + + + + dff.script.core.context — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.context

+"""
+Context
+-------
+A Context is a data structure that is used to store information about the current state of a conversation.
+It is used to keep track of the user's input, the current stage of the conversation, and any other
+information that is relevant to the current context of a dialog.
+The Context provides a convenient interface for working with data, allowing developers to easily add,
+retrieve, and manipulate data as the conversation progresses.
+
+The Context data structure provides several key features to make working with data easier.
+Developers can use the context to store any information that is relevant to the current conversation,
+such as user data, session data, conversation history, or etc.
+This allows developers to easily access and use this data throughout the conversation flow.
+
+Another important feature of the context is data serialization.
+The context can be easily serialized to a format that can be stored or transmitted, such as JSON.
+This allows developers to save the context data and resume the conversation later.
+"""
+import logging
+from uuid import UUID, uuid4
+
+from typing import Any, Optional, Union, Dict, List, Set
+
+from pydantic import BaseModel, Field, field_validator
+from .types import NodeLabel2Type, ModuleName
+from .message import Message
+
+logger = logging.getLogger(__name__)
+
+Node = BaseModel
+
+
+
+[docs] +def get_last_index(dictionary: dict) -> int: + """ + 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`. + """ + indices = list(dictionary) + return indices[-1] if indices else -1
+ + + +
+[docs] +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) + """ + `id` is the unique context identifier. By default, randomly generated using `uuid4` `id` is used. + `id` can be used to trace the user behavior, e.g while collecting the statistical data. + """ + labels: Dict[int, NodeLabel2Type] = {} + """ + `labels` stores the history of all passed `labels` + + - key - `id` of the turn. + - value - `label` on this turn. + """ + requests: Dict[int, Message] = {} + """ + `requests` stores the history of all `requests` received by the agent + + - key - `id` of the turn. + - value - `request` on this turn. + """ + responses: Dict[int, Message] = {} + """ + `responses` stores the history of all agent `responses` + + - key - `id` of the turn. + - value - `response` on this turn. + """ + misc: Dict[str, Any] = {} + """ + `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.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. + """ + framework_states: Dict[ModuleName, Dict[str, Any]] = {} + """ + `framework_states` is used for addons states or for + :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:`~.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. + """ + +
+[docs] + @field_validator("labels", "requests", "responses") + @classmethod + def sort_dict_keys(cls, dictionary: dict) -> dict: + """ + 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. + :return: Dictionary with sorted keys. + """ + return {key: dictionary[key] for key in sorted(dictionary)}
+ + +
+[docs] + @classmethod + def cast(cls, ctx: Optional[Union["Context", dict, str]] = None, *args, **kwargs) -> "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: 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: + ctx = Context(*args, **kwargs) + elif isinstance(ctx, dict): + ctx = Context.model_validate(ctx) + elif isinstance(ctx, str): + ctx = Context.model_validate_json(ctx) + elif not issubclass(type(ctx), Context): + raise ValueError( + 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
+ + +
+[docs] + def add_request(self, request: Message): + """ + 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. + """ + request_message = Message.model_validate(request) + last_index = get_last_index(self.requests) + self.requests[last_index + 1] = request_message
+ + +
+[docs] + def add_response(self, response: Message): + """ + 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. + """ + response_message = Message.model_validate(response) + last_index = get_last_index(self.responses) + self.responses[last_index + 1] = response_message
+ + +
+[docs] + def add_label(self, label: NodeLabel2Type): + """ + 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. + """ + last_index = get_last_index(self.labels) + self.labels[last_index + 1] = label
+ + +
+[docs] + def clear( + self, + hold_last_n_indices: int, + field_names: Union[Set[str], List[str]] = {"requests", "responses", "labels"}, + ): + """ + 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 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) + if "requests" in field_names: + for index in list(self.requests)[:-hold_last_n_indices]: + del self.requests[index] + if "responses" in field_names: + for index in list(self.responses)[:-hold_last_n_indices]: + del self.responses[index] + if "misc" in field_names: + self.misc.clear() + if "labels" in field_names: + for index in list(self.labels)[:-hold_last_n_indices]: + del self.labels[index] + if "framework_states" in field_names: + self.framework_states.clear()
+ + + @property + def last_label(self) -> Optional[NodeLabel2Type]: + """ + 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) + + @property + def last_response(self) -> Optional[Message]: + """ + 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) + + @last_response.setter + def last_response(self, response: Optional[Message]): + """ + Set the last `response` of the current :py:class:`~.Context`. + Required for use with various response wrappers. + """ + last_index = get_last_index(self.responses) + self.responses[last_index] = Message() if response is None else Message.model_validate(response) + + @property + def last_request(self) -> Optional[Message]: + """ + 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) + + @last_request.setter + def last_request(self, request: Optional[Message]): + """ + Set the last `request` of the current :py:class:`~.Context`. + Required for use with various request wrappers. + """ + last_index = get_last_index(self.requests) + self.requests[last_index] = Message() if request is None else Message.model_validate(request) + + @property + def current_node(self) -> Optional[Node]: + """ + Return current :py:class:`~dff.script.core.script.Node`. + """ + actor = self.framework_states.get("actor", {}) + node = ( + actor.get("processed_node") + or actor.get("pre_response_processed_node") + or actor.get("next_node") + or actor.get("pre_transitions_processed_node") + or actor.get("previous_node") + ) + if node is None: + logger.warning( + "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 + +
+[docs] + def overwrite_current_node_in_processing(self, processed_node: Node): + """ + 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` to set as the current node. + """ + is_processing = self.framework_states.get("actor", {}).get("processed_node") + if is_processing: + self.framework_states["actor"]["processed_node"] = Node.model_validate(processed_node) + else: + logger.warning( + f"The `{self.overwrite_current_node_in_processing.__name__}` " + "method can only be called from processing functions (either pre-response or pre-transition)." + )
+
+ + + +Context.model_rebuild() +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/keywords.html b/_modules/dff/script/core/keywords.html new file mode 100644 index 0000000000..75dac97f7d --- /dev/null +++ b/_modules/dff/script/core/keywords.html @@ -0,0 +1,683 @@ + + + + + + + + + + dff.script.core.keywords — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.keywords

+"""
+Keywords
+--------
+Keywords are used to define the dialog graph, which is the structure of a conversation.
+They are used to determine all nodes in the script and to assign python objects and python functions for nodes.
+
+"""
+from enum import Enum
+
+
+
+[docs] +class Keywords(str, Enum): + """ + Keywords used to define the dialog script (:py:class:`~dff.script.Script`). + The data type `dict` is used to describe the scenario. + `Enums` of this class are used as keys in this `dict`. + Different keys correspond to the different value types aimed at different purposes. + + Enums: + + GLOBAL: Enum(auto) + This keyword is used to define a global node. + The value that corresponds to this key has the `dict` type with keywords: + + `{TRANSITIONS:..., RESPONSE:..., PRE_RESPONSE_PROCESSING:..., MISC:...}`. + There can be only one global node in a script :py:class:`~dff.script.Script`. + The global node is defined at the flow level as opposed to regular nodes. + This node allows to define default global values for all nodes. + + LOCAL: Enum(auto) + This keyword is used to define the local node. + The value that corresponds to this key has the `dict` type with keywords: + + `{TRANSITIONS:..., RESPONSE:..., PRE_RESPONSE_PROCESSING:..., MISC:...}`. + The local node is defined in the same way as all other nodes in the flow of this node. + It also allows to redefine default values for all nodes in this node's flow. + + TRANSITIONS: Enum(auto) + This keyword defines possible transitions from node. + The value that corresponds to the `TRANSITIONS` key has the `dict` type. + Every key-value pair describes the transition node and the condition: + + `{label_to_transition_0: condition_for_transition_0, ..., label_to_transition_N: condition_for_transition_N}`, + + where `label_to_transition_i` is a node into which the actor make the transition in case of + `condition_for_transition_i == True`. + + RESPONSE: Enum(auto) + The keyword specifying the result which is returned to the user after getting to the node. + Value corresponding to the `RESPONSE` key can have any data type. + + MISC: Enum(auto) + The keyword specifying `dict` containing extra data, + which were not aimed to be used in the standard functions of `DFE`. + Value corresponding to the `MISC` key must have `dict` type: + + `{"VAR_KEY_0": VAR_VALUE_0, ..., "VAR_KEY_N": VAR_VALUE_N}`, + + where `"VAR_KEY_0"` is an arbitrary name of the value which is saved into the `MISC`. + + PRE_RESPONSE_PROCESSING: Enum(auto) + The keyword specifying the preprocessing that is called before the response generation. + The value that corresponds to the `PRE_RESPONSE_PROCESSING` key must have the `dict` type: + + `{"PRE_RESPONSE_PROC_0": pre_response_proc_func_0, ..., "PRE_RESPONSE_PROC_N": pre_response_proc__func_N}`, + + where `"PRE_RESPONSE_PROC_i"` is an arbitrary name of the preprocessing stage in the pipeline. + The order of `pre_response_proc__func_i` calls is defined by the order + in which the preprocessing `dict` is defined. + + PRE_TRANSITIONS_PROCESSING: Enum(auto) + The keyword specifying the preprocessing that is called before the transition. + The value that corresponds to the `PRE_TRANSITIONS_PROCESSING` key must have the `dict` type: + + `{"PRE_TRANSITIONS_PROC_0": pre_transitions_proc_func_0, ..., + "PRE_TRANSITIONS_PROC_N": pre_transitions_proc_func_N}`, + + where `"PRE_TRANSITIONS_PROC_i"` is an arbitrary name of the preprocessing stage in the pipeline. + The order of `pre_transitions_proc_func_i` calls is defined by the order + in which the preprocessing `dict` is defined. + + """ + + GLOBAL = "global" + LOCAL = "local" + TRANSITIONS = "transitions" + RESPONSE = "response" + MISC = "misc" + PRE_RESPONSE_PROCESSING = "pre_response_processing" + PRE_TRANSITIONS_PROCESSING = "pre_transitions_processing" + PROCESSING = "pre_transitions_processing"
+ + + +# Redefine shortcuts +GLOBAL = Keywords.GLOBAL +LOCAL = Keywords.LOCAL +TRANSITIONS = Keywords.TRANSITIONS +RESPONSE = Keywords.RESPONSE +MISC = Keywords.MISC +PRE_RESPONSE_PROCESSING = Keywords.PRE_RESPONSE_PROCESSING +PRE_TRANSITIONS_PROCESSING = Keywords.PRE_TRANSITIONS_PROCESSING +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/message.html b/_modules/dff/script/core/message.html new file mode 100644 index 0000000000..80c4a88873 --- /dev/null +++ b/_modules/dff/script/core/message.html @@ -0,0 +1,854 @@ + + + + + + + + + + dff.script.core.message — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.message

+"""
+Message
+-------
+The :py:class:`.Message` class is a universal data model for representing a message that should be supported by
+DFF. It only contains types and properties that are compatible with most messaging services.
+"""
+from typing import Any, Optional, List, Union
+from enum import Enum, auto
+from pathlib import Path
+from urllib.request import urlopen
+
+from pydantic import field_validator, Field, FilePath, HttpUrl, BaseModel, model_validator
+
+
+
+[docs] +class Session(Enum): + """ + An enumeration that defines two possible states of a session. + """ + + ACTIVE = auto() + FINISHED = auto()
+ + + +
+[docs] +class DataModel(BaseModel, extra="allow", arbitrary_types_allowed=True): + """ + This class is a Pydantic BaseModel that serves as a base class for all DFF models. + """ + + ...
+ + + +
+[docs] +class Command(DataModel): + """ + This class is a subclass of DataModel and represents + a command that can be executed in response to a user input. + """ + + ...
+ + + +
+[docs] +class Location(DataModel): + """ + This class is a data model that represents a geographical + location on the Earth's surface. + It has two attributes, longitude and latitude, both of which are float values. + If the absolute difference between the latitude and longitude values of the two + locations is less than 0.00004, they are considered equal. + """ + + longitude: float + latitude: float + + def __eq__(self, other): + if isinstance(other, Location): + return abs(self.latitude - other.latitude) + abs(self.longitude - other.longitude) < 0.00004 + return NotImplemented
+ + + +
+[docs] +class Attachment(DataModel): + """ + This class represents an attachment that can be either + a file or a URL, along with an optional ID and title. + """ + + source: Optional[Union[HttpUrl, FilePath]] = None + id: Optional[str] = None # id field is made separate to simplify type validation + title: Optional[str] = None + +
+[docs] + def get_bytes(self) -> Optional[bytes]: + if self.source is None: + return None + if isinstance(self.source, Path): + with open(self.source, "rb") as file: + return file.read() + else: + with urlopen(self.source.unicode_string()) as file: + return file.read()
+ + + def __eq__(self, other): + if isinstance(other, Attachment): + if self.title != other.title: + return False + if self.id != other.id: + return False + return self.get_bytes() == other.get_bytes() + return NotImplemented + +
+[docs] + @model_validator(mode="before") + @classmethod + def validate_source_or_id(cls, values: dict): + if not isinstance(values, dict): + raise AssertionError(f"Invalid constructor parameters: {str(values)}") + if bool(values.get("source")) == bool(values.get("id")): + raise AssertionError("Attachment type requires exactly one parameter, `source` or `id`, to be set.") + return values
+ + +
+[docs] + @field_validator("source", mode="before") + @classmethod + def validate_source(cls, value): + if isinstance(value, Path): + return Path(value) + return value
+
+ + + +
+[docs] +class Audio(Attachment): + """Represents an audio file attachment.""" + + pass
+ + + +
+[docs] +class Video(Attachment): + """Represents a video file attachment.""" + + pass
+ + + +
+[docs] +class Image(Attachment): + """Represents an image file attachment.""" + + pass
+ + + +
+[docs] +class Document(Attachment): + """Represents a document file attachment.""" + + pass
+ + + +
+[docs] +class Attachments(DataModel): + """This class is a data model that represents a list of attachments.""" + + files: List[Attachment] = Field(default_factory=list) + + def __eq__(self, other): + if isinstance(other, Attachments): + return self.files == other.files + return NotImplemented
+ + + + + + + +
+[docs] +class Button(DataModel): + """ + This class allows for the creation of a button object + with a source URL, a text description, and a payload. + """ + + source: Optional[HttpUrl] = None + text: str + payload: Optional[Any] = None + + def __eq__(self, other): + if isinstance(other, Button): + if self.source != other.source: + return False + if self.text != other.text: + return False + first_payload = bytes(self.payload, encoding="utf-8") if isinstance(self.payload, str) else self.payload + second_payload = bytes(other.payload, encoding="utf-8") if isinstance(other.payload, str) else other.payload + return first_payload == second_payload + return NotImplemented
+ + + +
+[docs] +class Keyboard(DataModel): + """ + This class is a DataModel that represents a keyboard object + that can be used for a chatbot or messaging application. + """ + + buttons: List[Button] = Field(default_factory=list, min_length=1) + + def __eq__(self, other): + if isinstance(other, Keyboard): + return self.buttons == other.buttons + return NotImplemented
+ + + +
+[docs] +class Message(DataModel): + """ + Class representing a message and contains several + class level variables to store message information. + """ + + text: Optional[str] = None + commands: Optional[List[Command]] = None + attachments: Optional[Attachments] = None + annotations: Optional[dict] = None + misc: Optional[dict] = None + # commands and state options are required for integration with services + # that use an intermediate backend server, like Yandex's Alice + # state: Optional[Session] = Session.ACTIVE + # ui: Optional[Union[Keyboard, DataModel]] = None + + def __eq__(self, other): + if isinstance(other, Message): + for field in self.model_fields: + if field not in other.model_fields: + return False + if self.__getattribute__(field) != other.__getattribute__(field): + return False + return True + return NotImplemented + + def __repr__(self) -> str: + return " ".join([f"{key}='{value}'" for key, value in self.model_dump(exclude_none=True).items()])
+ + + +
+[docs] +class MultiMessage(Message): + """This class represents a message that contains multiple sub-messages.""" + + messages: Optional[List[Message]] = None
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/normalization.html b/_modules/dff/script/core/normalization.html new file mode 100644 index 0000000000..455bafc258 --- /dev/null +++ b/_modules/dff/script/core/normalization.html @@ -0,0 +1,724 @@ + + + + + + + + + + dff.script.core.normalization — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.normalization

+"""
+Normalization
+-------------
+Normalization module is used to normalize all python objects and functions to a format
+that is suitable for script and actor execution process.
+This module contains a basic set of functions for normalizing data in a dialog script.
+"""
+import logging
+
+from typing import Union, Callable, Any, Dict, Optional, ForwardRef
+
+from .keywords import Keywords
+from .context import Context
+from .types import NodeLabel3Type, NodeLabelType, ConditionType, LabelType
+from .message import Message
+
+from pydantic import validate_call
+
+logger = logging.getLogger(__name__)
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +def normalize_label(label: NodeLabelType, default_flow_label: LabelType = "") -> Union[Callable, NodeLabel3Type]: + """ + The function that is used for normalization of + :py:const:`default_flow_label <dff.script.NodeLabelType>`. + + :param label: If label is Callable the function is wrapped into try/except + and normalization is used on the result of the function call with the name label. + :param default_flow_label: flow_label is used if label does not contain flow_label. + :return: Result of the label normalization, + if Callable is returned, the normalized result is returned. + """ + if callable(label): + + def get_label_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + try: + new_label = label(ctx, pipeline, *args, **kwargs) + new_label = normalize_label(new_label, default_flow_label) + flow_label, node_label, _ = new_label + node = pipeline.script.get(flow_label, {}).get(node_label) + if not node: + raise Exception(f"Unknown transitions {new_label} for pipeline.script={pipeline.script}") + if node_label in [Keywords.LOCAL, Keywords.GLOBAL]: + raise Exception(f"Invalid transition: can't transition to {flow_label}:{node_label}") + except Exception as exc: + new_label = None + logger.error(f"Exception {exc} of function {label}", exc_info=exc) + return new_label + + return get_label_handler # create wrap to get uniq key for dictionary + elif isinstance(label, str) or isinstance(label, Keywords): + return (default_flow_label, label, float("-inf")) + elif isinstance(label, tuple) and len(label) == 2 and isinstance(label[-1], float): + return (default_flow_label, label[0], label[-1]) + elif isinstance(label, tuple) and len(label) == 2 and isinstance(label[-1], str): + flow_label = label[0] or default_flow_label + return (flow_label, label[-1], float("-inf")) + elif isinstance(label, tuple) and len(label) == 3: + flow_label = label[0] or default_flow_label + return (flow_label, label[1], label[2])
+ + + +
+[docs] +def normalize_condition(condition: ConditionType) -> Callable: + """ + The function that is used to normalize `condition` + + :param condition: Condition to normalize. + :return: The function condition wrapped into the try/except. + """ + if callable(condition): + + def callable_condition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + try: + return condition(ctx, pipeline, *args, **kwargs) + except Exception as exc: + logger.error(f"Exception {exc} of function {condition}", exc_info=exc) + return False + + return callable_condition_handler
+ + + +
+[docs] +@validate_call +def normalize_response(response: Optional[Union[Message, Callable[..., Message]]]) -> Callable[..., Message]: + """ + This function is used to normalize response, if response Callable, it is returned, otherwise + response is wrapped to the function and this function is returned. + + :param response: Response to normalize. + :return: Function that returns callable response. + """ + if callable(response): + return response + else: + if response is None: + result = Message() + elif isinstance(response, Message): + result = response + else: + raise TypeError(type(response)) + + def response_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs): + return result + + return response_handler
+ + + +
+[docs] +@validate_call +def normalize_processing(processing: Dict[Any, Callable]) -> Callable: + """ + This function is used to normalize processing. + It returns function that consecutively applies all preprocessing stages from dict. + + :param processing: Processing which contains all preprocessing stages in a format "PROC_i" -> proc_func_i. + :return: Function that consequentially applies all preprocessing stages from dict. + """ + if isinstance(processing, dict): + + def processing_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + for processing_name, processing_func in processing.items(): + try: + if processing_func is not None: + ctx = processing_func(ctx, pipeline, *args, **kwargs) + except Exception as exc: + logger.error( + f"Exception {exc} for processing_name={processing_name} and processing_func={processing_func}", + exc_info=exc, + ) + return ctx + + return processing_handler
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/script.html b/_modules/dff/script/core/script.html new file mode 100644 index 0000000000..b2013b6729 --- /dev/null +++ b/_modules/dff/script/core/script.html @@ -0,0 +1,746 @@ + + + + + + + + + + dff.script.core.script — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.script

+"""
+Script
+------
+The Script module provides a set of `pydantic` models for representing the dialog graph.
+These models are used to define the conversation flow, and to determine the appropriate response based on
+the user's input and the current state of the conversation.
+"""
+# %%
+
+import logging
+from typing import Callable, Optional, Any, Dict, Union
+
+from pydantic import BaseModel, field_validator
+
+from .types import LabelType, NodeLabelType, ConditionType, NodeLabel3Type
+from .message import Message
+from .keywords import Keywords
+from .normalization import normalize_response, normalize_processing, normalize_condition, normalize_label, validate_call
+from typing import ForwardRef
+
+logger = logging.getLogger(__name__)
+
+
+Pipeline = ForwardRef("Pipeline")
+Context = ForwardRef("Context")
+
+
+
+[docs] +class Node(BaseModel, extra="forbid"): + """ + The class for the `Node` object. + """ + + transitions: Dict[NodeLabelType, ConditionType] = {} + response: Optional[Union[Message, Callable[[Context, Pipeline], Message]]] = None + pre_transitions_processing: Dict[Any, Callable] = {} + pre_response_processing: Dict[Any, Callable] = {} + misc: dict = {} + +
+[docs] + @field_validator("transitions", mode="before") + @classmethod + @validate_call + def normalize_transitions( + cls, transitions: Dict[NodeLabelType, ConditionType] + ) -> Dict[Union[Callable, NodeLabel3Type], Callable]: + """ + The function which is used to normalize transitions and returns normalized dict. + + :param transitions: Transitions to normalize. + :return: Transitions with normalized label and condition. + """ + transitions = { + normalize_label(label): normalize_condition(condition) for label, condition in transitions.items() + } + return transitions
+ + +
+[docs] + def run_response(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + """ + Executes the normalized response. + See details in the :py:func:`~normalize_response` function of `normalization.py`. + """ + response = normalize_response(self.response) + return response(ctx, pipeline, *args, **kwargs)
+ + +
+[docs] + def run_pre_response_processing(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + """ + Executes pre-processing of responses. + """ + return self.run_processing(self.pre_response_processing, ctx, pipeline, *args, **kwargs)
+ + +
+[docs] + def run_pre_transitions_processing(self, ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Context: + """ + Executes pre-processing of transitions. + """ + return self.run_processing(self.pre_transitions_processing, ctx, pipeline, *args, **kwargs)
+ + +
+[docs] + def run_processing( + self, processing: Dict[Any, Callable], ctx: Context, pipeline: Pipeline, *args, **kwargs + ) -> Context: + """ + Executes the normalized processing. + See details in the :py:func:`~normalize_processing` function of `normalization.py`. + """ + processing = normalize_processing(processing) + return processing(ctx, pipeline, *args, **kwargs)
+
+ + + +
+[docs] +class Script(BaseModel, extra="forbid"): + """ + The class for the `Script` object. + """ + + script: Dict[LabelType, Dict[LabelType, Node]] + +
+[docs] + @field_validator("script", mode="before") + @classmethod + @validate_call + def normalize_script(cls, script: Dict[LabelType, Any]) -> Dict[LabelType, Dict[LabelType, Dict[str, Any]]]: + """ + This function normalizes :py:class:`.Script`: it returns dict where the GLOBAL node is moved + into the flow with the GLOBAL name. The function returns the structure + + `{GLOBAL: {...NODE...}, ...}` -> `{GLOBAL: {GLOBAL: {...NODE...}}, ...}`. + + :param script: :py:class:`.Script` that describes the dialog scenario. + :return: Normalized :py:class:`.Script`. + """ + if isinstance(script, dict): + if Keywords.GLOBAL in script and all( + [isinstance(item, Keywords) for item in script[Keywords.GLOBAL].keys()] + ): + script[Keywords.GLOBAL] = {Keywords.GLOBAL: script[Keywords.GLOBAL]} + return script
+ + + def __getitem__(self, key): + return self.script[key] + +
+[docs] + def get(self, key, value=None): + return self.script.get(key, value)
+ + +
+[docs] + def keys(self): + return self.script.keys()
+ + +
+[docs] + def items(self): + return self.script.items()
+ + +
+[docs] + def values(self): + return self.script.values()
+ + + def __iter__(self): + return self.script.__iter__()
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/core/types.html b/_modules/dff/script/core/types.html new file mode 100644 index 0000000000..63447d13c7 --- /dev/null +++ b/_modules/dff/script/core/types.html @@ -0,0 +1,696 @@ + + + + + + + + + + dff.script.core.types — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.core.types

+"""
+Types
+-----
+The Types module contains a set of basic data types that
+are used to define the expected input and output of various components of the framework.
+The types defined in this module include basic data types such as strings
+and lists, as well as more complex types that are specific to the framework.
+"""
+from typing import Union, Callable, Tuple
+from enum import Enum, auto
+from typing_extensions import TypeAlias
+
+from .keywords import Keywords
+
+LabelType: TypeAlias = Union[str, Keywords]
+"""Label can be a casual string or :py:class:`~dff.script.Keywords`."""
+
+NodeLabel1Type: TypeAlias = Tuple[str, float]
+"""Label type for transitions can be `[node_name, transition_priority]`."""
+
+NodeLabel2Type: TypeAlias = Tuple[str, str]
+"""Label type for transitions can be `[flow_name, node_name]`."""
+
+NodeLabel3Type: TypeAlias = Tuple[str, str, float]
+"""Label type for transitions can be `[flow_name, node_name, transition_priority]`."""
+
+NodeLabelTupledType: TypeAlias = Union[NodeLabel1Type, NodeLabel2Type, NodeLabel3Type]
+"""Label type for transitions can be one of three different types."""
+
+NodeLabelType: TypeAlias = Union[Callable, NodeLabelTupledType, str]
+"""Label type for transitions can be one of three different types."""
+
+ConditionType: TypeAlias = Callable
+"""Condition type can be only `Callable`."""
+
+ModuleName: TypeAlias = "str"
+"""
+Module name names addon state, or your own module state. For example module name can be `"dff_context_storages"`.
+"""
+# TODO: change example
+
+
+
+[docs] +class ActorStage(Enum): + """ + The class which holds keys for the handlers. These keys are used + for the actions of :py:class:`.Actor`. Each stage represents + a specific step in the conversation flow. Here is a brief description + of each stage. + """ + + CONTEXT_INIT = auto() + """ + This stage is used for the context initialization. + It involves setting up the conversation context. + """ + + GET_PREVIOUS_NODE = auto() + """ + This stage is used to retrieve the previous node. + """ + + REWRITE_PREVIOUS_NODE = auto() + """ + This stage is used to rewrite the previous node. + It involves updating the previous node in the conversation history + to reflect any changes made during the current conversation turn. + """ + + RUN_PRE_TRANSITIONS_PROCESSING = auto() + """ + This stage is used for running pre-transitions processing. + It involves performing any necessary pre-processing tasks. + """ + + GET_TRUE_LABELS = auto() + """ + This stage is used to retrieve the true labels. + It involves determining the correct label to take based + on the current conversation context. + """ + + GET_NEXT_NODE = auto() + """ + This stage is used to retrieve the next node in the conversation flow. + """ + + REWRITE_NEXT_NODE = auto() + """ + This stage is used to rewrite the next node. + It involves updating the next node in the conversation flow + to reflect any changes made during the current conversation turn. + """ + + RUN_PRE_RESPONSE_PROCESSING = auto() + """ + This stage is used for running pre-response processing. + It involves performing any necessary pre-processing tasks + before generating the response to the user. + """ + + CREATE_RESPONSE = auto() + """ + This stage is used for response creation. + It involves generating a response to the user based on the + current conversation context and any pre-processing performed. + """ + + FINISH_TURN = auto() + """ + This stage is used for finishing the current conversation turn. + It involves wrapping up any loose ends, such as saving context, + before waiting for the user's next input. + """
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/labels/std_labels.html b/_modules/dff/script/labels/std_labels.html new file mode 100644 index 0000000000..5810be1061 --- /dev/null +++ b/_modules/dff/script/labels/std_labels.html @@ -0,0 +1,779 @@ + + + + + + + + + + dff.script.labels.std_labels — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.labels.std_labels

+"""
+Labels
+------
+:py:const:`Labels <dff.script.NodeLabel3Type>` are one of the important components of the dialog graph,
+which determine the targeted node name of the transition.
+They are used to identify the next step in the conversation.
+Labels can also be used in combination with other conditions,
+such as the current context or user data, to create more complex and dynamic conversations.
+
+This module contains a standard set of scripting :py:const:`labels <dff.script.NodeLabelType>` that
+can be used by developers to define the conversation flow.
+"""
+from typing import Optional, Callable, ForwardRef
+from dff.script import Context, NodeLabel3Type
+
+Pipeline = ForwardRef("Pipeline")
+
+
+
+[docs] +def repeat(priority: Optional[float] = None) -> Callable: + """ + Returns transition handler that takes :py:class:`.Context`, + :py:class:`~dff.pipeline.Pipeline` and :py:const:`priority <float>`. + This handler returns a :py:const:`label <NodeLabelType>` + to the last node with a given :py:const:`priority <float>`. + If the priority is not given, `Pipeline.actor.label_priority` is used as default. + + :param priority: Priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + """ + + def repeat_transition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + current_priority = pipeline.actor.label_priority if priority is None else priority + if len(ctx.labels) >= 1: + flow_label, label = list(ctx.labels.values())[-1] + else: + flow_label, label = pipeline.actor.start_label[:2] + return (flow_label, label, current_priority) + + return repeat_transition_handler
+ + + + + + + +
+[docs] +def to_start(priority: Optional[float] = None) -> Callable: + """ + Returns transition handler that takes :py:class:`~dff.script.Context`, + :py:class:`~dff.pipeline.Pipeline` and :py:const:`priority <float>`. + This handler returns a :py:const:`label <dff.script.NodeLabelType>` + to the start node with a given :py:const:`priority <float>`. + If the priority is not given, `Pipeline.actor.label_priority` is used as default. + + :param priority: Priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + """ + + def to_start_transition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + current_priority = pipeline.actor.label_priority if priority is None else priority + return (*pipeline.actor.start_label[:2], current_priority) + + return to_start_transition_handler
+ + + +
+[docs] +def to_fallback(priority: Optional[float] = None) -> Callable: + """ + Returns transition handler that takes :py:class:`~dff.script.Context`, + :py:class:`~dff.pipeline.Pipeline` and :py:const:`priority <float>`. + This handler returns a :py:const:`label <dff.script.NodeLabelType>` + to the fallback node with a given :py:const:`priority <float>`. + If the priority is not given, `Pipeline.actor.label_priority` is used as default. + + :param priority: Priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + """ + + def to_fallback_transition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + current_priority = pipeline.actor.label_priority if priority is None else priority + return (*pipeline.actor.fallback_label[:2], current_priority) + + return to_fallback_transition_handler
+ + + +
+[docs] +def _get_label_by_index_shifting( + ctx: Context, + pipeline: Pipeline, + priority: Optional[float] = None, + increment_flag: bool = True, + cyclicality_flag: bool = True, + *args, + **kwargs, +) -> NodeLabel3Type: + """ + Function that returns node label from the context and pipeline after shifting the index. + + :param ctx: Dialog context. + :param pipeline: Dialog pipeline. + :param priority: Priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + :param increment_flag: If it is `True`, label index is incremented by `1`, + otherwise it is decreased by `1`. Defaults to `True`. + :param cyclicality_flag: If it is `True` the iteration over the label list is going cyclically + (e.g the element with `index = len(labels)` has `index = 0`). Defaults to `True`. + :return: The tuple that consists of `(flow_label, label, priority)`. + If fallback is executed `(flow_fallback_label, fallback_label, priority)` are returned. + """ + flow_label, node_label, current_priority = repeat(priority)(ctx, pipeline, *args, **kwargs) + labels = list(pipeline.script.get(flow_label, {})) + + if node_label not in labels: + return (*pipeline.actor.fallback_label[:2], current_priority) + + label_index = labels.index(node_label) + label_index = label_index + 1 if increment_flag else label_index - 1 + if not (cyclicality_flag or (0 <= label_index < len(labels))): + return (*pipeline.actor.fallback_label[:2], current_priority) + label_index %= len(labels) + + return (flow_label, labels[label_index], current_priority)
+ + + +
+[docs] +def forward(priority: Optional[float] = None, cyclicality_flag: bool = True) -> Callable: + """ + Returns transition handler that takes :py:class:`~dff.script.Context`, + :py:class:`~dff.pipeline.Pipeline` and :py:const:`priority <float>`. + This handler returns a :py:const:`label <dff.script.NodeLabelType>` + to the forward node with a given :py:const:`priority <float>` and :py:const:`cyclicality_flag <bool>`. + If the priority is not given, `Pipeline.actor.label_priority` is used as default. + + :param priority: Float priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + :param cyclicality_flag: If it is `True`, the iteration over the label list is going cyclically + (e.g the element with `index = len(labels)` has `index = 0`). Defaults to `True`. + """ + + def forward_transition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + return _get_label_by_index_shifting( + ctx, pipeline, priority, increment_flag=True, cyclicality_flag=cyclicality_flag, *args, **kwargs + ) + + return forward_transition_handler
+ + + +
+[docs] +def backward(priority: Optional[float] = None, cyclicality_flag: bool = True) -> Callable: + """ + Returns transition handler that takes :py:class:`~dff.script.Context`, + :py:class:`~dff.pipeline.Pipeline` and :py:const:`priority <float>`. + This handler returns a :py:const:`label <dff.script.NodeLabelType>` + to the backward node with a given :py:const:`priority <float>` and :py:const:`cyclicality_flag <bool>`. + If the priority is not given, `Pipeline.actor.label_priority` is used as default. + + :param priority: Float priority of transition. Uses `Pipeline.actor.label_priority` if priority not defined. + :param cyclicality_flag: If it is `True`, the iteration over the label list is going cyclically + (e.g the element with `index = len(labels)` has `index = 0`). Defaults to `True`. + """ + + def back_transition_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> NodeLabel3Type: + return _get_label_by_index_shifting( + ctx, pipeline, priority, increment_flag=False, cyclicality_flag=cyclicality_flag, *args, **kwargs + ) + + return back_transition_handler
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/script/responses/std_responses.html b/_modules/dff/script/responses/std_responses.html new file mode 100644 index 0000000000..bd406f029d --- /dev/null +++ b/_modules/dff/script/responses/std_responses.html @@ -0,0 +1,612 @@ + + + + + + + + + + dff.script.responses.std_responses — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.script.responses.std_responses

+"""
+Responses
+---------
+Responses determine the response that will be sent to the user for each node of the dialog graph.
+Responses are used to provide the user with information, ask questions,
+or guide the conversation in a particular direction.
+
+This module provides only one predefined response function that can be used to quickly
+respond to the user and keep the conversation flowing.
+"""
+import random
+from typing import List
+
+from dff.pipeline import Pipeline
+from dff.script import Context, Message
+
+
+
+[docs] +def choice(responses: List[Message]): + """ + Function wrapper that takes the list of responses as an input + and returns handler which outputs a response randomly chosen from that list. + + :param responses: A list of responses for random sampling. + """ + + def choice_response_handler(ctx: Context, pipeline: Pipeline, *args, **kwargs): + return random.choice(responses) + + return choice_response_handler
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/stats/cli.html b/_modules/dff/stats/cli.html new file mode 100644 index 0000000000..9d3cc314dd --- /dev/null +++ b/_modules/dff/stats/cli.html @@ -0,0 +1,847 @@ + + + + + + + + + + dff.stats.cli — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.stats.cli

+"""
+Command Line Interface
+----------------------
+This modules defines commands that can be called via the command line interface.
+
+"""
+from uuid import uuid4
+import tempfile
+import shutil
+import sys
+import argparse
+import os
+import logging
+from urllib import parse
+from pathlib import Path
+from typing import Optional
+
+try:
+    from omegaconf import OmegaConf
+    from .utils import get_superset_session, drop_superset_assets
+except ImportError:
+    raise ImportError("Some packages are not found. Run `pip install dff[stats]`")
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+DFF_DIR = Path(__file__).absolute().parent.parent
+"""
+Root directory of the local `dff` installation.
+
+:meta hide-value:
+"""
+DASHBOARD_DIR = str(DFF_DIR / "config" / "superset_dashboard")
+"""
+Local path to superset dashboard files to import.
+
+:meta hide-value:
+"""
+DASHBOARD_SLUG = "dff-stats"
+"""
+This variable stores a slug used for building the http address of the DFF dashboard.
+"""
+DEFAULT_SUPERSET_URL = parse.urlunsplit(("http", "localhost:8088", "/", "", ""))
+"""
+Default location of the Superset dashboard.
+"""
+
+TYPE_MAPPING_CH = {
+    "FLOAT": "Nullable(Float64)",
+    "STRING": "Nullable(String)",
+    "LONGINTEGER": "Nullable(Int64)",
+    "INTEGER": "Nullable(Int64)",
+    "DATETIME": "Nullable(DateTime)",
+}
+"""
+Mapping of standard sql column types to Clickhouse native types.
+
+:meta hide-value:
+"""
+
+DFF_NODE_STATS_STATEMENT = """
+WITH main AS (
+    SELECT DISTINCT {table}.LogAttributes['context_id'] as context_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,
+    {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 and data_key = 'get_current_label'
+    ORDER BY 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_STATS_STATEMENT = """
+WITH main AS (
+    SELECT DISTINCT {table}.LogAttributes['context_id'] as context_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,
+    {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(toUInt64OrNull(LogAttributes['request_id'])) AS max_history
+    FROM {table}\nGROUP BY context_id
+)
+SELECT DISTINCT LogAttributes['context_id'] AS context_id,
+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
+FROM {table}
+INNER JOIN main
+ON context_id  = main.context_id
+AND request_id = main.max_history
+INNER JOIN otel_traces
+ON {table}.TraceId = otel_traces.TraceId
+WHERE otel_traces.SpanName = 'get_current_label'
+"""
+
+SQL_STATEMENT_MAPPING = {
+    "dff_stats.yaml": DFF_STATS_STATEMENT,
+    "dff_node_stats.yaml": DFF_NODE_STATS_STATEMENT,
+    "dff_final_nodes.yaml": DFF_FINAL_NODES_STATEMENT,
+}
+"""
+Select statements for dashboard configuration with names and types represented as placeholders.
+The placeholder system makes queries database agnostic, required values are set during the import phase.
+
+:meta hide-value:
+"""
+
+
+
+[docs] +def import_dashboard(parsed_args: Optional[argparse.Namespace] = None, zip_file: Optional[str] = None): + """ + Import an Apache Superset dashboard to a local instance with specified arguments. + Before using the command, make sure you have your Superset instance + up and running: `ghcr.io/deeppavlov/superset_df_dashboard:latest`. + The import will override existing dashboard configurations if present. + + :param parsed_args: Command line arguments produced by `argparse`. + :param zip_file: Zip archived dashboard config. + """ + host = parsed_args.host if hasattr(parsed_args, "host") else "localhost" + port = parsed_args.port if hasattr(parsed_args, "port") else "8088" + superset_url = parse.urlunsplit(("http", f"{host}:{port}", "/", "", "")) + zip_filename = os.path.basename(zip_file) + db_password = getattr(parsed_args, "db.password") + + session, headers = get_superset_session(parsed_args, superset_url) + drop_superset_assets(session, headers, superset_url) + import_dashboard_url = parse.urljoin(superset_url, "/api/v1/dashboard/import/") + # upload files + with open(zip_file, "rb") as f: + response = session.request( + "POST", + import_dashboard_url, + headers=headers, + data={ + "passwords": '{"databases/dff_database.yaml":"' + db_password + '"}', + "overwrite": "true", + }, + files=[("formData", (zip_filename, f, "application/zip"))], + ) + response.raise_for_status() + logger.info(f"Upload finished with status {response.status_code}.")
+ + + +
+[docs] +def make_zip_config(parsed_args: argparse.Namespace) -> Path: + """ + Make a zip-archived Apache Superset dashboard config, using specified arguments. + + :param parsed_args: Command line arguments produced by `argparse`. + """ + if hasattr(parsed_args, "outfile") and parsed_args.outfile: + outfile_name = parsed_args.outfile + else: + 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] + cmd_conf = OmegaConf.from_cli() + cli_conf = OmegaConf.merge(file_conf, cmd_conf) + + if OmegaConf.select(cli_conf, "db.driver") == "clickhousedb+connect": + params = dict( + table="${db.table}", + 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')", + nodefield="JSON_VALUE(${db.table}.Body, '$.node')", + ) + else: + raise ValueError("The only supported database driver is 'clickhousedb+connect'.") + + conf = SQL_STATEMENT_MAPPING.copy() + for key in conf.keys(): + conf[key] = {} + conf[key]["sql"] = SQL_STATEMENT_MAPPING[key].format(**params) + + resolve_conf = OmegaConf.create( + { + "database": { + "sqlalchemy_uri": "${db.driver}://${db.user}:XXXXXXXXXX@${db.host}:${db.port}/${db.name}", + }, + **conf, + } + ) + + user_config = OmegaConf.merge(cli_conf, resolve_conf) + OmegaConf.resolve(user_config) + + with tempfile.TemporaryDirectory() as temp_config_dir: + nested_temp_dir = os.path.join(temp_config_dir, "superset_dashboard") + logger.info(f"Copying config files to temporary directory: {nested_temp_dir}.") + + shutil.copytree(DASHBOARD_DIR, nested_temp_dir) + database_dir = Path(os.path.join(nested_temp_dir, "databases")) + dataset_dir = Path(os.path.join(nested_temp_dir, "datasets/dff_database")) + + logger.info("Overriding the initial configuration.") + # overwrite sqlalchemy uri + for filepath in database_dir.iterdir(): + file_config = OmegaConf.load(filepath) + new_file_config = OmegaConf.merge(file_config, OmegaConf.select(user_config, "database")) + OmegaConf.save(new_file_config, filepath) + + # overwrite sql expressions and column types + for filepath in dataset_dir.iterdir(): + file_config = OmegaConf.load(filepath) + new_file_config = OmegaConf.merge(file_config, getattr(user_config, filepath.name)) + if OmegaConf.select(cli_conf, "db.driver") == "clickhousedb+connect": + for col in OmegaConf.select(new_file_config, "columns"): + col.type = TYPE_MAPPING_CH.get(col.type, col.type) + OmegaConf.save(new_file_config, filepath) + + if ".zip" not in outfile_name: + raise ValueError(f"Outfile name missing .zip extension: {outfile_name}.") + logger.info(f"Saving the archive to {outfile_name}.") + shutil.make_archive(outfile_name[: outfile_name.rindex(".zip")], format="zip", root_dir=temp_config_dir) + + return Path(outfile_name)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/stats/default_extractors.html b/_modules/dff/stats/default_extractors.html new file mode 100644 index 0000000000..08b4e13dc8 --- /dev/null +++ b/_modules/dff/stats/default_extractors.html @@ -0,0 +1,685 @@ + + + + + + + + + + dff.stats.default_extractors — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.stats.default_extractors

+"""
+Default Extractors
+------------------
+This module includes a pool of default extractors
+that you can use out of the box.
+
+The default configuration for Superset dashboard leverages the data collected
+by the extractors below. In order to use the default charts,
+make sure that you include those functions in your pipeline.
+Detailed examples can be found in the `tutorials` section.
+
+"""
+from datetime import datetime
+
+from dff.script import Context
+from dff.pipeline import ExtraHandlerRuntimeInfo, Pipeline
+from .utils import get_wrapper_field
+
+
+
+[docs] +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: + last_label = pipeline.actor.start_label[:2] + return {"flow": last_label[0], "node": last_label[1], "label": ": ".join(last_label)}
+ + + +
+[docs] +async def get_timing_before(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + """ + Extract the pipeline component's start time. + This function is required for running the dashboard with the default configuration. + + The function leverages the `framework_states` field of the context to store results. + As a result, the function output is cleared on every turn and does not get persisted + to the context storage. + """ + start_time = datetime.now() + ctx.framework_states[get_wrapper_field(info, "time")] = start_time
+ + + +
+[docs] +async def get_timing_after(ctx: Context, _, info: ExtraHandlerRuntimeInfo): # noqa: F811 + """ + Extract the pipeline component's finish time. + This function is required for running the dashboard with the default configuration. + + The function leverages the `framework_states` field of the context to store results. + As a result, the function output is cleared on every turn and does not get persisted + to the context storage. + """ + start_time = ctx.framework_states[get_wrapper_field(info, "time")] + data = {"execution_time": str(datetime.now() - start_time)} + return data
+ + + +
+[docs] +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
+ + + +
+[docs] +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: +""" +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/stats/instrumentor.html b/_modules/dff/stats/instrumentor.html new file mode 100644 index 0000000000..b1dcce0f27 --- /dev/null +++ b/_modules/dff/stats/instrumentor.html @@ -0,0 +1,791 @@ + + + + + + + + + + dff.stats.instrumentor — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.stats.instrumentor

+"""
+Instrumentor
+-------------
+This modules contains the :py:class:`~OtelInstrumentor` class that implements
+Opentelemetry's `BaseInstrumentor` interface and allows for automated
+instrumentation of Dialog Flow Framework applications,
+e.g. for automated logging and log export.
+
+For detailed reference, see `~OtelInstrumentor` class.
+"""
+import asyncio
+from typing import Collection, Optional
+
+from wrapt import wrap_function_wrapper, decorator
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.metrics import get_meter, get_meter_provider, Meter
+from opentelemetry.trace import get_tracer, get_tracer_provider, Tracer
+from opentelemetry._logs import get_logger, get_logger_provider, Logger, SeverityNumber
+from opentelemetry.trace import SpanKind, Span
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk._logs import LoggerProvider, LogRecord
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
+
+from dff.script.core.context import get_last_index
+from dff.stats.utils import (
+    resource,
+    get_wrapper_field,
+    set_logger_destination,
+    set_meter_destination,
+    set_tracer_destination,
+)
+from dff.stats import default_extractors
+
+
+INSTRUMENTS = ["dff"]
+
+
+
+[docs] +class OtelInstrumentor(BaseInstrumentor): + """ + Utility class for instrumenting DFF-related functions + that implements the :py:class:`~BaseInstrumentor` interface. + :py:meth:`~instrument` and :py:meth:`~uninstrument` methods + are available to apply and revert the instrumentation effects, + e.g. enable and disable logging at runtime. + + .. code-block:: + + dff_instrumentor = OtelInstrumentor() + dff_instrumentor.instrument() + dff_instrumentor.uninstrument() + + Opentelemetry provider instances can be optionally passed to the class constructor. + Otherwise, the global logger, tracer and meter providers are leveraged. + + The class implements the :py:meth:`~__call__` method, so that + regular functions can be decorated using the class instance. + + .. code-block:: + + @dff_instrumentor + async def function(context, pipeline, runtime_info): + ... + + :param logger_provider: Opentelemetry logger provider. Used to construct a logger instance. + :param tracer_provider: Opentelemetry tracer provider. Used to construct a tracer instance. + :parame meter_provider: Opentelemetry meter provider. Used to construct a meter instance. + """ + + def __init__(self, logger_provider=None, tracer_provider=None, meter_provider=None) -> None: + super().__init__() + self._logger_provider: Optional[LoggerProvider] = None + self._tracer_provider: Optional[TracerProvider] = None + self._meter_provider: Optional[MeterProvider] = None + self._logger: Optional[Logger] = None + self._tracer: Optional[Tracer] = None + self._meter: Optional[Meter] = None + self._configure_providers( + logger_provider=logger_provider, tracer_provider=tracer_provider, meter_provider=meter_provider + ) + + def __enter__(self): + if not self.is_instrumented_by_opentelemetry: + self.instrument() + return self + + def __exit__(self): + if self.is_instrumented_by_opentelemetry: + self.uninstrument() + +
+[docs] + @classmethod + def from_url(cls, url: str, insecure: bool = True, timeout: Optional[int] = None): + """ + Construct an instrumentor instance using only the url of the OTLP Collector. + Inherently modifies the global provider instances adding an export destination + for the target url. + + .. code-block:: + + instrumentor = OtelInstrumentor.from_url("grpc://localhost:4317") + + :param url: Url of the running Otel Collector server. Due to limited support of HTTP protocol + by the Opentelemetry Python extension, GRPC protocol is preferred. + :param insecure: Whether non-SSL-protected connection is allowed. Defaults to True. + :param timeout: Connection timeout in seconds, optional. + """ + set_logger_destination(OTLPLogExporter(endpoint=url, insecure=insecure, timeout=timeout)) + set_tracer_destination(OTLPSpanExporter(endpoint=url, insecure=insecure, timeout=timeout)) + set_meter_destination(OTLPMetricExporter(endpoint=url, insecure=insecure, timeout=timeout)) + return cls()
+ + +
+[docs] + def instrumentation_dependencies(self) -> Collection[str]: + """ + :meta private: + + Required libraries. Implements the Python Opentelemetry instrumentor interface. + + """ + return INSTRUMENTS
+ + +
+[docs] + def _instrument(self, logger_provider=None, tracer_provider=None, meter_provider=None): + if any([logger_provider, meter_provider, tracer_provider]): + self._configure_providers( + logger_provider=logger_provider, tracer_provider=tracer_provider, meter_provider=meter_provider + ) + for func_name in default_extractors.__all__: + wrap_function_wrapper(default_extractors, func_name, self.__call__.__wrapped__)
+ + +
+[docs] + def _uninstrument(self, **kwargs): + for func_name in default_extractors.__all__: + unwrap(default_extractors, func_name)
+ + +
+[docs] + def _configure_providers(self, logger_provider, tracer_provider, meter_provider): + self._logger_provider = logger_provider or get_logger_provider() + self._tracer_provider = tracer_provider or get_tracer_provider() + self._meter_provider = meter_provider or get_meter_provider() + self._logger = get_logger(__name__, None, self._logger_provider) + self._tracer = get_tracer(__name__, None, self._tracer_provider) + self._meter = get_meter(__name__, None, self._meter_provider)
+ + + @decorator + async def __call__(self, wrapped, _, args, kwargs): + """ + Regular functions that match the :py:class:`~dff.pipeline.types.ExtraHandlerFunction` + signature can be decorated with the class instance to log the returned value. + This method implements the logging procedure. + The returned value is assumed to be `dict` or `NoneType`. + Logging non-atomic values is discouraged, as they cannot be translated using + the `Protobuf` protocol. + Logging is ignored if the application is in 'uninstrumented' state. + + :param wrapped: Function to decorate. + :param args: Positional arguments of the decorated function. + :param kwargs: Keyword arguments of the decorated function. + """ + ctx, _, info = args + pipeline_component = get_wrapper_field(info) + attributes = { + "context_id": str(ctx.id), + "request_id": get_last_index(ctx.requests), + "pipeline_component": pipeline_component, + } + + result: Optional[dict] + if asyncio.iscoroutinefunction(wrapped): + result = await wrapped(ctx, _, info) + else: + result = wrapped(ctx, _, info) + + if result is None or not self.is_instrumented_by_opentelemetry: + # self.is_instrumented_by_opentelemetry allows to disable + # the decorator programmatically if + # instrumentation is disabled. + return result + + span: Span + with self._tracer.start_as_current_span(wrapped.__name__, kind=SpanKind.INTERNAL) as span: + span_ctx = span.get_span_context() + record = LogRecord( + span_id=span_ctx.span_id, + trace_id=span_ctx.trace_id, + body=result, + trace_flags=span_ctx.trace_flags, + severity_text=None, + severity_number=SeverityNumber(1), + resource=resource, + attributes=attributes, + ) + self._logger.emit(record=record) + return result
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/stats/utils.html b/_modules/dff/stats/utils.html new file mode 100644 index 0000000000..0359e15321 --- /dev/null +++ b/_modules/dff/stats/utils.html @@ -0,0 +1,803 @@ + + + + + + + + + + dff.stats.utils — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.stats.utils

+"""
+Utils
+-----
+This module includes utility functions designed for statistics collection.
+
+The functions below can be used to configure the opentelemetry destination.
+
+.. code:: python
+
+    set_logger_destination(OTLPLogExporter("grpc://localhost:4317", insecure=True))
+    set_tracer_destination(OTLPSpanExporter("grpc://localhost:4317", insecure=True))
+
+"""
+import json
+import getpass
+from urllib import parse
+from typing import Optional, Tuple
+from argparse import Namespace, Action
+
+import requests
+from opentelemetry.sdk.resources import Resource
+from opentelemetry._logs import get_logger_provider, set_logger_provider
+from opentelemetry.trace import get_tracer_provider, set_tracer_provider
+from opentelemetry.metrics import get_meter_provider, set_meter_provider
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk._logs import LoggerProvider
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
+from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter, SpanExporter
+from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter, LogExporter
+from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter, MetricExporter
+
+from dff.pipeline import ExtraHandlerRuntimeInfo
+
+SERVICE_NAME = "dialog_flow_framework"
+
+resource = Resource.create({"service.name": SERVICE_NAME})
+"""
+Singletone :py:class:`~Resource` instance shared inside the framework.
+"""
+tracer_provider = TracerProvider(resource=resource)
+"""
+Global tracer provider bound to the DFF resource.
+"""
+logger_provider = LoggerProvider(resource=resource)
+"""
+Global logger provider bound to the DFF resource.
+"""
+set_logger_provider(logger_provider)
+set_tracer_provider(tracer_provider)
+
+
+
+[docs] +def set_logger_destination(exporter: Optional[LogExporter] = None): + """ + Configure the global Opentelemetry logger provider to export logs to the given destination. + + :param exporter: Opentelemetry log exporter instance. + """ + if exporter is None: + exporter = OTLPLogExporter(endpoint="grpc://localhost:4317", insecure=True) + get_logger_provider().add_log_record_processor(BatchLogRecordProcessor(exporter))
+ + + +
+[docs] +def set_meter_destination(exporter: Optional[MetricExporter] = None): + """ + Configure the global Opentelemetry meter provider to export metrics to the given destination. + + :param exporter: Opentelemetry meter exporter instance. + """ + if exporter is None: + exporter = OTLPMetricExporter(endpoint="grpc://localhost:4317", insecure=True) + cur_meter_provider = get_meter_provider() + new_meter_provider = MeterProvider(resource=resource, metric_readers=[PeriodicExportingMetricReader(exporter)]) + if not isinstance(cur_meter_provider, MeterProvider): + set_meter_provider(new_meter_provider)
+ + + +
+[docs] +def set_tracer_destination(exporter: Optional[SpanExporter] = None): + """ + Configure the global Opentelemetry tracer provider to export traces to the given destination. + + :param exporter: Opentelemetry span exporter instance. + """ + if exporter is None: + exporter = OTLPSpanExporter(endpoint="grpc://localhost:4317", insecure=True) + get_tracer_provider().add_span_processor(BatchSpanProcessor(exporter))
+ + + +
+[docs] +def get_wrapper_field(info: ExtraHandlerRuntimeInfo, postfix: str = "") -> str: + """ + This function can be used to obtain a key, under which the wrapper data will be stored + in the context. + + :param info: Handler runtime info obtained from the pipeline. + :param postfix: Field-specific postfix that will be appended to the field name. + """ + path = info.component.path.replace(".", "-") + return f"{path}" + (f"-{postfix}" if postfix else "")
+ + + +
+[docs] +def get_superset_session(args: Namespace, base_url: str = "http://localhost:8088/") -> Tuple[requests.Session, dict]: + """ + Utility function for authorized interaction with Superset HTTP API. + + :param args: Command line arguments including Superset username and Superset password. + :param base_url: Base Superset URL. + + :return: Authorized session - authorization headers tuple. + """ + healthcheck_url = parse.urljoin(base_url, "/healthcheck") + login_url = parse.urljoin(base_url, "/api/v1/security/login") + csrf_url = parse.urljoin(base_url, "/api/v1/security/csrf_token/") + + session = requests.Session() + # do healthcheck + response = session.get(healthcheck_url, timeout=10) + response.raise_for_status() + # get access token + access_request = session.post( + login_url, + headers={"Content-Type": "application/json", "Accept": "*/*"}, + data=json.dumps({"username": args.username, "password": args.password, "refresh": True, "provider": "db"}), + ) + access_token = access_request.json()["access_token"] + # get csrf_token + csrf_request = session.get(csrf_url, headers={"Authorization": f"Bearer {access_token}"}) + csrf_token = csrf_request.json()["result"] + headers = { + "Authorization": f"Bearer {access_token}", + "X-CSRFToken": csrf_token, + } + return session, headers
+ + + +
+[docs] +def drop_superset_assets(session: requests.Session, headers: dict, base_url: str): + """ + Drop the existing assets from the Superset dashboard. + + :param session: Authorized Superset session. + :param headers: Superset session headers. + :param base_url: Base Superset URL. + """ + dashboard_url = parse.urljoin(base_url, "/api/v1/dashboard") + charts_url = parse.urljoin(base_url, "/api/v1/chart") + datasets_url = parse.urljoin(base_url, "/api/v1/dataset") + database_url = parse.urljoin(base_url, "/api/v1/database/") + delete_res: requests.Response + + dashboard_res = session.get(dashboard_url, headers=headers) + dashboard_json = dashboard_res.json() + if dashboard_json["count"] > 0: + delete_res = requests.delete(dashboard_url, params={"q": json.dumps(dashboard_json["ids"])}, headers=headers) + delete_res.raise_for_status() + + charts_result = session.get(charts_url, headers=headers) + charts_json = charts_result.json() + if charts_json["count"] > 0: + delete_res = requests.delete(charts_url, params={"q": json.dumps(charts_json["ids"])}, headers=headers) + delete_res.raise_for_status() + + datasets_result = session.get(datasets_url, headers=headers) + datasets_json = datasets_result.json() + if datasets_json["count"] > 0: + delete_res = requests.delete(datasets_url, params={"q": json.dumps(datasets_json["ids"])}, headers=headers) + delete_res.raise_for_status() + + database_res = session.get(database_url, headers=headers) + database_json = database_res.json() + if database_json["count"] > 0: + delete_res = requests.delete(database_url + str(database_json["ids"][-1]), headers=headers) + delete_res.raise_for_status()
+ + + +
+[docs] +class PasswordAction(Action): + """ + Child class for Argparse's :py:class:`~Action` that prompts users for passwords interactively, + ensuring password safety, unless the password is specified directly. + + """ + + def __init__( + self, option_strings, dest=None, nargs=0, default=None, required=False, type=None, metavar=None, help=None + ): + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=nargs, + default=default, + required=required, + metavar=metavar, + type=type, + help=help, + ) + + def __call__(self, parser, args, values, option_string=None): + if values: + print(f"{self.dest}: setting passwords explicitly through the command line is discouraged.") + setattr(args, self.dest, values) + else: + setattr(args, self.dest, getpass.getpass(prompt=f"{self.dest}: "))
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/db_benchmark/basic_config.html b/_modules/dff/utils/db_benchmark/basic_config.html new file mode 100644 index 0000000000..5584c4bbfa --- /dev/null +++ b/_modules/dff/utils/db_benchmark/basic_config.html @@ -0,0 +1,819 @@ + + + + + + + + + + dff.utils.db_benchmark.basic_config — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.db_benchmark.basic_config

+"""
+Basic Config
+------------
+This module contains basic benchmark configurations.
+
+It defines a simple configurations class (:py:class:`~.BasicBenchmarkConfig`)
+as well as a set of configurations that covers different dialogs a user might have and some edge-cases
+(:py:data:`~.basic_configurations`).
+"""
+from typing import Tuple, Optional
+import string
+import random
+
+from humanize import naturalsize
+from pympler import asizeof
+
+from dff.script import Message, Context
+from dff.utils.db_benchmark.benchmark import BenchmarkConfig
+
+
+
+[docs] +def get_dict(dimensions: Tuple[int, ...]): + """ + Return misc dictionary build in `dimensions` dimensions. + + :param dimensions: + Dimensions of the dictionary. + Each element of the dimensions tuple is the number of keys on the corresponding level of the dictionary. + The last element of the dimensions tuple is the length of the string values of the dict. + + e.g. dimensions=(1, 2) returns a dictionary with 1 key that points to a string of len 2. + whereas dimensions=(1, 2, 3) returns a dictionary with 1 key that points to a dictionary + with 2 keys each of which points to a string of len 3. + + So, the len of dimensions is the depth of the dictionary, while its values are + the width of the dictionary at each level. + """ + + def _get_dict(dimensions: Tuple[int, ...]): + if len(dimensions) < 2: + # get a random string of length dimensions[0] + return "".join(random.choice(string.printable) for _ in range(dimensions[0])) + return {str(i): _get_dict(dimensions[1:]) for i in range(dimensions[0])} + + if len(dimensions) > 1: + return _get_dict(dimensions) + elif len(dimensions) == 1: + return _get_dict((dimensions[0], 0)) + else: + return _get_dict((0, 0))
+ + + +
+[docs] +def get_message(message_dimensions: Tuple[int, ...]): + """ + Return message with a non-empty misc field. + + :param message_dimensions: Dimensions of the misc field of the message. See :py:func:`~.get_dict`. + """ + return Message(misc=get_dict(message_dimensions))
+ + + +
+[docs] +def get_context( + dialog_len: int, + message_dimensions: Tuple[int, ...], + misc_dimensions: Tuple[int, ...], +) -> Context: + """ + Return context with a non-empty misc, labels, requests, responses fields. + + :param dialog_len: Number of labels, requests and responses. + :param message_dimensions: + A parameter used to generate messages for requests and responses. See :py:func:`~.get_message`. + :param misc_dimensions: + A parameter used to generate misc field. See :py:func:`~.get_dict`. + """ + return Context( + labels={i: (f"flow_{i}", f"node_{i}") for i in range(dialog_len)}, + requests={i: get_message(message_dimensions) for i in range(dialog_len)}, + responses={i: get_message(message_dimensions) for i in range(dialog_len)}, + misc=get_dict(misc_dimensions), + )
+ + + +
+[docs] +class BasicBenchmarkConfig(BenchmarkConfig, frozen=True): + """ + A simple benchmark configuration that generates contexts using two parameters: + + - `message_dimensions` -- to configure the way messages are generated. + - `misc_dimensions` -- to configure size of context's misc field. + + Dialog length is configured using `from_dialog_len`, `to_dialog_len`, `step_dialog_len`. + """ + + context_num: int = 30 + """ + Number of times the contexts will be benchmarked. + Increasing this number decreases standard error of the mean for benchmarked data. + """ + from_dialog_len: int = 300 + """Starting dialog len of a context.""" + to_dialog_len: int = 311 + """ + Final dialog len of a context. + :py:meth:`~.BasicBenchmarkConfig.context_updater` will return contexts + until their dialog len is less then `to_dialog_len`. + """ + step_dialog_len: int = 1 + """ + Increment step for dialog len. + :py:meth:`~.BasicBenchmarkConfig.context_updater` will return contexts + increasing dialog len by `step_dialog_len`. + """ + message_dimensions: Tuple[int, ...] = (10, 10) + """ + Dimensions of misc dictionaries inside messages. + See :py:func:`~.get_message`. + """ + misc_dimensions: Tuple[int, ...] = (10, 10) + """ + Dimensions of misc dictionary. + See :py:func:`~.get_dict`. + """ + +
+[docs] + def get_context(self) -> Context: + """ + Return context with `from_dialog_len`, `message_dimensions`, `misc_dimensions`. + + Wraps :py:func:`~.get_context`. + """ + return get_context(self.from_dialog_len, self.message_dimensions, self.misc_dimensions)
+ + +
+[docs] + def info(self): + """ + Return fields of this instance and sizes of objects defined by this config. + + :return: + A dictionary with two keys. + Key "params" stores fields of this configuration. + Key "sizes" stores string representation of following values: + + - "starting_context_size" -- size of a context with `from_dialog_len`. + - "final_context_size" -- size of a context with `to_dialog_len`. + A context of this size will never actually be benchmarked. + - "misc_size" -- size of a misc field of a context. + - "message_size" -- size of a misc field of a message. + """ + return { + "params": self.model_dump(), + "sizes": { + "starting_context_size": naturalsize(asizeof.asizeof(self.get_context()), gnu=True), + "final_context_size": naturalsize( + asizeof.asizeof(get_context(self.to_dialog_len, self.message_dimensions, self.misc_dimensions)), + gnu=True, + ), + "misc_size": naturalsize(asizeof.asizeof(get_dict(self.misc_dimensions)), gnu=True), + "message_size": naturalsize(asizeof.asizeof(get_message(self.message_dimensions)), gnu=True), + }, + }
+ + +
+[docs] + def context_updater(self, context: Context) -> Optional[Context]: + """ + Update context to have `step_dialog_len` more labels, requests and responses, + unless such dialog len would be equal to `to_dialog_len` or exceed than it, + in which case None is returned. + """ + start_len = len(context.labels) + if start_len + self.step_dialog_len < self.to_dialog_len: + for i in range(start_len, start_len + self.step_dialog_len): + context.add_label((f"flow_{i}", f"node_{i}")) + context.add_request(get_message(self.message_dimensions)) + context.add_response(get_message(self.message_dimensions)) + return context + else: + return None
+
+ + + +basic_configurations = { + "large-misc": BasicBenchmarkConfig( + from_dialog_len=1, + to_dialog_len=50, + message_dimensions=(3, 5, 6, 5, 3), + misc_dimensions=(2, 4, 3, 8, 100), + ), + "short-messages": BasicBenchmarkConfig( + from_dialog_len=500, + to_dialog_len=550, + message_dimensions=(2, 30), + misc_dimensions=(0, 0), + ), + "default": BasicBenchmarkConfig(), + "large-misc--long-dialog": BasicBenchmarkConfig( + from_dialog_len=500, + to_dialog_len=550, + message_dimensions=(3, 5, 6, 5, 3), + misc_dimensions=(2, 4, 3, 8, 100), + ), + "very-long-dialog-len": BasicBenchmarkConfig( + context_num=10, + from_dialog_len=10000, + to_dialog_len=10050, + ), + "very-long-message-len": BasicBenchmarkConfig( + context_num=10, + from_dialog_len=1, + to_dialog_len=3, + message_dimensions=(10000, 1), + ), + "very-long-misc-len": BasicBenchmarkConfig( + context_num=10, + from_dialog_len=1, + to_dialog_len=3, + misc_dimensions=(10000, 1), + ), +} +""" +Configuration that covers many dialog cases (as well as some edge-cases). + +:meta hide-value: +""" +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/db_benchmark/benchmark.html b/_modules/dff/utils/db_benchmark/benchmark.html new file mode 100644 index 0000000000..bec71224c6 --- /dev/null +++ b/_modules/dff/utils/db_benchmark/benchmark.html @@ -0,0 +1,1034 @@ + + + + + + + + + + dff.utils.db_benchmark.benchmark — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.db_benchmark.benchmark

+"""
+Context storage benchmarking
+----------------------------
+This module contains functions for context storages benchmarking.
+
+The basic function is :py:func:`~.time_context_read_write` but it has a low level interface.
+
+Higher level wrappers of the function provided by this module are:
+
+- :py:func:`~.save_results_to_file` -- saves results for a list of benchmark cases.
+- :py:func:`~.benchmark_all` -- a wrapper of `save_results_to_file`. Creates cases from configs.
+
+Wrappers use :py:class:`~.BenchmarkConfig` interface to configure benchmarks.
+A simple configuration class as well as a configuration set are provided by
+:py:mod:`dff.utils.db_benchmark.basic_config`.
+
+To view files generated by :py:func:`~.save_results_to_file` use either
+:py:func:`~dff.utils.db_benchmark.report.report` or
+`our streamlit app <../_misc/benchmark_streamlit.py>`_.
+"""
+from uuid import uuid4
+from pathlib import Path
+from time import perf_counter
+from typing import Tuple, List, Dict, Union, Optional, Callable, Any
+import json
+import importlib
+from statistics import mean
+import abc
+from traceback import extract_tb, StackSummary
+
+from pydantic import BaseModel, Field
+from tqdm.auto import tqdm
+
+from dff.context_storages import DBContextStorage
+from dff.script import Context
+
+
+
+[docs] +def time_context_read_write( + context_storage: DBContextStorage, + context_factory: Callable[[], Context], + context_num: int, + context_updater: Optional[Callable[[Context], Optional[Context]]] = None, +) -> Tuple[List[float], List[Dict[int, float]], List[Dict[int, float]]]: + """ + Benchmark `context_storage` by writing and reading `context`\\s generated by `context_factory` + into it / from it `context_num` times. + If `context_updater` is not `None` it is used to update `context`\\s and benchmark update operation. + + This function clears `context_storage` before and after execution. + + :param context_storage: Context storage to benchmark. + :param context_factory: A function that creates contexts which will be written into context storage. + :param context_num: A number of times the context will be written and read. + :param context_updater: + None or a function. + If not None, function should accept :py:class:`~.Context` and return an updated :py:class:`~.Context`. + The updated context can be either the same object (at the same pointer) or a different object (e.g. copied). + The updated context should have a higher dialog length than the received context + (to emulate context updating during dialog). + The function should return `None` to stop updating contexts. + For an example of such function, see implementation of + :py:meth:`dff.utils.db_benchmark.basic_config.BasicBenchmarkConfig.context_updater`. + + To avoid keeping many contexts in memory, + this function will be called repeatedly at least `context_num` times. + :return: + A tuple of 3 elements. + + The first element -- a list of write times. Its length is equal to `context_num`. + + The second element -- a list of dictionaries with read times. + Each dictionary maps from int to float. The key in the mapping is the `dialog_len` of the context and the + values are the read times for the corresponding `dialog_len`. + If `context_updater` is None, all dictionaries will have only one key -- + dialog length of the context returned by `context_factory`. + Otherwise, the dictionaries will also have a key for each updated context. + + The third element -- a list of dictionaries with update times. + Structurally the same as the second element, but none of the elements here have a key for + dialog_len of the context returned by `context_factory`. + So if `context_updater` is None, all dictionaries will be empty. + """ + context_storage.clear() + + write_times: List[float] = [] + read_times: List[Dict[int, float]] = [] + update_times: List[Dict[int, float]] = [] + + for _ in tqdm(range(context_num), desc=f"Benchmarking context storage:{context_storage.full_path}", leave=False): + context = context_factory() + + ctx_id = uuid4() + + # write operation benchmark + write_start = perf_counter() + context_storage[ctx_id] = context + write_times.append(perf_counter() - write_start) + + read_times.append({}) + update_times.append({}) + + # read operation benchmark + read_start = perf_counter() + _ = context_storage[ctx_id] + read_time = perf_counter() - read_start + read_times[-1][len(context.labels)] = read_time + + if context_updater is not None: + updated_context = context_updater(context) + + while updated_context is not None: + update_start = perf_counter() + context_storage[ctx_id] = updated_context + update_time = perf_counter() - update_start + update_times[-1][len(updated_context.labels)] = update_time + + read_start = perf_counter() + _ = context_storage[ctx_id] + read_time = perf_counter() - read_start + read_times[-1][len(updated_context.labels)] = read_time + + updated_context = context_updater(updated_context) + + context_storage.clear() + return write_times, read_times, update_times
+ + + +
+[docs] +class DBFactory(BaseModel): + """ + A class for storing information about context storage to benchmark. + Also used to create a context storage from the configuration. + """ + + uri: str + """URI of the context storage.""" + factory_module: str = "dff.context_storages" + """A module containing `factory`.""" + factory: str = "context_storage_factory" + """Name of the context storage factory. (function that creates context storages from URIs)""" + +
+[docs] + def db(self): + """ + Create a context storage using `factory` from `uri`. + """ + module = importlib.import_module(self.factory_module) + return getattr(module, self.factory)(self.uri)
+
+ + + +
+[docs] +class BenchmarkConfig(BaseModel, abc.ABC, frozen=True): + """ + Configuration for a benchmark. + + Defines methods and parameters required to run :py:func:`~.time_context_read_write`. + Also defines a method (`info`) for displaying information about this configuration. + + A simple way to configure benchmarks is provided by + :py:class:`~.dff.utils.db_benchmark.basic_config.BasicBenchmarkConfig`. + + Inherit from this class only if `BasicBenchmarkConfig` is not enough for your benchmarking needs. + """ + + context_num: int = 30 + """ + Number of times the contexts will be benchmarked. + Increasing this number decreases standard error of the mean for benchmarked data. + """ + +
+[docs] + @abc.abstractmethod + def get_context(self) -> Context: + """ + Return context to benchmark read and write operations with. + + This function will be called `context_num` times. + """ + ...
+ + +
+[docs] + @abc.abstractmethod + def info(self) -> Dict[str, Any]: + """ + Return a dictionary with information about this configuration. + """ + ...
+ + +
+[docs] + @abc.abstractmethod + def context_updater(self, context: Context) -> Optional[Context]: + """ + Update context with new dialog turns or return `None` to stop updates. + + This function is used to benchmark update and read operations. + + This function will be called AT LEAST `context_num` times. + + :return: Updated context or `None` to stop updating context. + """ + ...
+
+ + + +
+[docs] +class BenchmarkCase(BaseModel): + """ + This class represents a benchmark case and includes + information about it, its configuration and configuration of a context storage to benchmark. + """ + + name: str + """Name of a benchmark case.""" + db_factory: DBFactory + """DBFactory that specifies context storage to benchmark.""" + benchmark_config: BenchmarkConfig + """Benchmark configuration.""" + uuid: str = Field(default_factory=lambda: str(uuid4())) + """Unique id of the case. Defaults to a random uuid.""" + description: str = "" + """Description of the case. Defaults to an empty string.""" + +
+[docs] + @staticmethod + def set_average_results(benchmark): + """ + Modify `benchmark` dictionary to include averaged benchmark results. + + Add field "average_results" to the benchmark that contains the following fields: + + - average_write_time + - average_read_time + - average_update_time + - read_times_grouped_by_context_num -- a list of read times. + Each element is the average of read times with the same context_num. + - read_times_grouped_by_dialog_len -- a dictionary of read times. + Its values are the averages of read times with the same dialog_len, + its keys are dialog_len values. + - update_times_grouped_by_context_num + - update_times_grouped_by_dialog_len + - pretty_write -- average write time with only 3 significant digits. + - pretty_read + - pretty_update + - pretty_read+update -- sum of average read and update times with only 3 significant digits. + + :param benchmark: + A dictionary returned by `BenchmarkCase._run`. + Should include a "success" and "result" fields. + "success" field should be true. + "result" field should be a dictionary with the values returned by + :py:func:`~.time_context_read_write` and keys + "write_times", "read_times" and "update_times". + :return: None + """ + if not benchmark["success"] or isinstance(benchmark["result"], str): + return + + def get_complex_stats(results): + if len(results) == 0 or len(results[0]) == 0: + return [], {}, None + + average_grouped_by_context_num = [mean(times.values()) for times in results] + average_grouped_by_dialog_len = {key: mean([times[key] for times in results]) for key in results[0].keys()} + average = float(mean(average_grouped_by_context_num)) + return average_grouped_by_context_num, average_grouped_by_dialog_len, average + + read_stats = get_complex_stats(benchmark["result"]["read_times"]) + update_stats = get_complex_stats(benchmark["result"]["update_times"]) + + result = { + "average_write_time": mean(benchmark["result"]["write_times"]), + "average_read_time": read_stats[2], + "average_update_time": update_stats[2], + "read_times_grouped_by_context_num": read_stats[0], + "read_times_grouped_by_dialog_len": read_stats[1], + "update_times_grouped_by_context_num": update_stats[0], + "update_times_grouped_by_dialog_len": update_stats[1], + } + result["pretty_write"] = ( + float(f'{result["average_write_time"]:.3}') if result["average_write_time"] is not None else None + ) + result["pretty_read"] = ( + float(f'{result["average_read_time"]:.3}') if result["average_read_time"] is not None else None + ) + result["pretty_update"] = ( + float(f'{result["average_update_time"]:.3}') if result["average_update_time"] is not None else None + ) + result["pretty_read+update"] = ( + float(f'{result["average_read_time"] + result["average_update_time"]:.3}') + if result["average_read_time"] is not None and result["average_update_time"] is not None + else None + ) + + benchmark["average_results"] = result
+ + +
+[docs] + def _run(self): + try: + write_times, read_times, update_times = time_context_read_write( + self.db_factory.db(), + self.benchmark_config.get_context, + self.benchmark_config.context_num, + self.benchmark_config.context_updater, + ) + return { + "success": True, + "result": { + "write_times": write_times, + "read_times": read_times, + "update_times": update_times, + }, + } + except Exception as e: + return { + "success": False, + "result": { + "type": e.__class__.__name__, + "msg": getattr(e, "message", str(e)), + "traceback": "\n".join(StackSummary.from_list(extract_tb(e.__traceback__)).format()), + }, + }
+ + +
+[docs] + def run(self): + """ + Run benchmark, return results. + + :return: + A dictionary with 3 keys: "success", "result", "average_results". + + Success is a bool value. It is false if an exception was raised during benchmarking. + + Result is either an exception message or a dictionary with 3 keys + ("write_times", "read_times", "update_times"). + Values of those fields are the values returned by :py:func:`~.time_context_read_write`. + + Average results field is as described in :py:meth:`~.BenchmarkCase.set_average_results`. + """ + benchmark = self._run() + BenchmarkCase.set_average_results(benchmark) + return benchmark
+
+ + + +
+[docs] +def save_results_to_file( + benchmark_cases: List[BenchmarkCase], + file: Union[str, Path], + name: str, + description: str, + exist_ok: bool = False, +): + """ + Benchmark all `benchmark_cases` and save results to a file. + + Result are saved in json format with this schema: + `utils/db_benchmark/benchmark_schema.json <../_misc/benchmark_schema.json>`_. + + Files created by this function cen be viewed either by using :py:func:`~dff.utils.db_benchmark.report.report` or + streamlit app located in the utils directory: + `utils/db_benchmark/benchmark_streamlit.py <../_misc/benchmark_streamlit.py>`_. + + :param benchmark_cases: A list of benchmark cases that specify benchmarks. + :param file: File to save results to. + :param name: Name of the benchmark set. + :param description: Description of the benchmark set. + :param exist_ok: Whether to continue if the file already exists. + """ + with open(file, "w" if exist_ok else "x", encoding="utf-8") as fd: + uuid = str(uuid4()) + result: Dict[str, Any] = { + "name": name, + "description": description, + "uuid": uuid, + "benchmarks": [], + } + cases = tqdm(benchmark_cases, leave=False) + case: BenchmarkCase + for case in cases: + cases.set_description(f"Benchmarking: {case.name}") + result["benchmarks"].append( + { + **case.model_dump(exclude={"benchmark_config"}), + "benchmark_config": case.benchmark_config.info(), + **case.run(), + } + ) + + json.dump(result, fd)
+ + + +
+[docs] +def benchmark_all( + file: Union[str, Path], + name: str, + description: str, + db_uri: str, + benchmark_configs: Dict[str, BenchmarkConfig], + exist_ok: bool = False, +): + """ + A wrapper for :py:func:`~.save_results_to_file`. + + Generates `benchmark_cases` from `db_uri` and `benchmark_configs`: + `db_uri` is used to initialize :py:class:`~.DBFactory` instance + which is then used along with `benchmark_configs` to initialize :py:class:`~.BenchmarkCase` instances. + + :param file: File to save results to. + :param name: Name of the benchmark set. + :param description: Description of the benchmark set. The same description is used for benchmark cases. + :param db_uri: URI of the database to benchmark + :param benchmark_configs: Mapping from case names to configs. + :param exist_ok: Whether to continue if the file already exists. + """ + save_results_to_file( + [ + BenchmarkCase( + name=case_name, + description=description, + db_factory=DBFactory(uri=db_uri), + benchmark_config=benchmark_config, + ) + for case_name, benchmark_config in benchmark_configs.items() + ], + file, + name, + description, + exist_ok=exist_ok, + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/db_benchmark/report.html b/_modules/dff/utils/db_benchmark/report.html new file mode 100644 index 0000000000..d5517b4169 --- /dev/null +++ b/_modules/dff/utils/db_benchmark/report.html @@ -0,0 +1,646 @@ + + + + + + + + + + dff.utils.db_benchmark.report — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.db_benchmark.report

+"""
+Report
+--------
+This method contains a function to print benchmark results to console.
+"""
+from pathlib import Path
+from typing import Union, Set, Literal
+import json
+
+
+
+[docs] +def report( + file: Union[str, Path], + display: Set[Literal["name", "desc", "config", "metrics"]] = set({"name", "metrics"}), +): + """ + Print average results from a result file to stdout. + + Printed stats contain benchmark configs, object sizes, average benchmark values for successful cases and + exception message for unsuccessful cases. + + :param file: + File with benchmark results generated by + :py:func:`~dff.utils.db_benchmark.benchmark.save_results_to_file`. + :param display: + A set of objects to display in results. + Values allowed inside the set: + + - "name" -- displays the name of the benchmark case. + - "desc" -- displays the description of the benchmark case. + - "config" -- displays the config info of the benchmark case. + - "metrics" -- displays average write, read, update read+update times. + """ + with open(file, "r", encoding="utf-8") as fd: + file_contents = json.load(fd) + + sep = "-" * 80 + + report_result = "\n".join([sep, file_contents["name"], sep, file_contents["description"], sep, ""]) + + for benchmark in file_contents["benchmarks"]: + reported_values = { + "name": benchmark["name"], + "desc": benchmark["description"], + "config": "\n".join(f"{k}: {v}" for k, v in benchmark["benchmark_config"].items()), + "metrics": "".join( + [ + f"{metric.title() + ': ' + str(benchmark['average_results']['pretty_' + metric]):20}" + if benchmark["success"] + else benchmark["result"] + for metric in ("write", "read", "update", "read+update") + ] + ), + } + + result = [] + for value_name, value in reported_values.items(): + if value_name in display: + result.append(value) + result.append("") + + report_result += f"\n{sep}\n".join(result) + + print(report_result, end="")
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/testing/cleanup_db.html b/_modules/dff/utils/testing/cleanup_db.html new file mode 100644 index 0000000000..1c8b49ecfa --- /dev/null +++ b/_modules/dff/utils/testing/cleanup_db.html @@ -0,0 +1,713 @@ + + + + + + + + + + dff.utils.testing.cleanup_db — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.testing.cleanup_db

+"""
+Cleanup DB
+----------
+This module defines functions that allow to delete data in various types of databases,
+including JSON, MongoDB, Pickle, Redis, Shelve, SQL, and YDB databases.
+"""
+import os
+
+from dff.context_storages import (
+    JSONContextStorage,
+    MongoContextStorage,
+    PickleContextStorage,
+    RedisContextStorage,
+    ShelveContextStorage,
+    SQLContextStorage,
+    YDBContextStorage,
+    json_available,
+    mongo_available,
+    pickle_available,
+    redis_available,
+    sqlite_available,
+    postgres_available,
+    mysql_available,
+    ydb_available,
+)
+
+
+
+[docs] +async def delete_json(storage: JSONContextStorage): + """ + Delete all data from a JSON context storage. + + :param storage: A JSONContextStorage object. + """ + if not json_available: + raise Exception("Can't delete JSON database - JSON provider unavailable!") + if os.path.isfile(storage.path): + os.remove(storage.path)
+ + + +
+[docs] +async def delete_mongo(storage: MongoContextStorage): + """ + Delete all data from a MongoDB context storage. + + :param storage: A MongoContextStorage object + """ + if not mongo_available: + raise Exception("Can't delete mongo database - mongo provider unavailable!") + await storage.collection.drop()
+ + + +
+[docs] +async def delete_pickle(storage: PickleContextStorage): + """ + Delete all data from a Pickle context storage. + + :param storage: A PickleContextStorage object. + """ + if not pickle_available: + raise Exception("Can't delete pickle database - pickle provider unavailable!") + if os.path.isfile(storage.path): + os.remove(storage.path)
+ + + +
+[docs] +async def delete_redis(storage: RedisContextStorage): + """ + Delete all data from a Redis context storage. + + :param storage: A RedisContextStorage object. + """ + if not redis_available: + raise Exception("Can't delete redis database - redis provider unavailable!") + await storage.clear_async()
+ + + +
+[docs] +async def delete_shelve(storage: ShelveContextStorage): + """ + Delete all data from a Shelve context storage. + + :param storage: A ShelveContextStorage object. + """ + if os.path.isfile(storage.path): + os.remove(storage.path)
+ + + +
+[docs] +async def delete_sql(storage: SQLContextStorage): + """ + Delete all data from an SQL context storage. + + :param storage: An SQLContextStorage object. + """ + if storage.dialect == "postgres" and not postgres_available: + raise Exception("Can't delete postgres database - postgres provider unavailable!") + if storage.dialect == "sqlite" and not sqlite_available: + raise Exception("Can't delete sqlite database - sqlite provider unavailable!") + if storage.dialect == "mysql" and not mysql_available: + raise Exception("Can't delete mysql database - mysql provider unavailable!") + async with storage.engine.connect() as conn: + await conn.run_sync(storage.table.drop, storage.engine)
+ + + +
+[docs] +async def delete_ydb(storage: YDBContextStorage): + """ + Delete all data from a YDB context storage. + + :param storage: A YDBContextStorage object. + """ + if not ydb_available: + raise Exception("Can't delete ydb database - ydb provider unavailable!") + + async def callee(session): + await session.drop_table("/".join([storage.database, storage.table_name])) + + await storage.pool.retry_operation(callee)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/testing/common.html b/_modules/dff/utils/testing/common.html new file mode 100644 index 0000000000..3922231ae6 --- /dev/null +++ b/_modules/dff/utils/testing/common.html @@ -0,0 +1,674 @@ + + + + + + + + + + dff.utils.testing.common — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.testing.common

+"""
+Common
+------
+This module contains several functions which are used to run demonstrations in tutorials.
+"""
+from os import getenv
+from typing import Callable, Tuple, Any, Optional
+from uuid import uuid4
+
+from dff.script import Context, Message
+from dff.pipeline import Pipeline
+from dff.utils.testing.response_comparers import default_comparer
+
+
+
+[docs] +def is_interactive_mode() -> bool: # pragma: no cover + """ + Checking whether the tutorial code should be run in interactive mode. + + :return: `True` if it's being executed by Jupyter kernel and DISABLE_INTERACTIVE_MODE env variable isn't set, + `False` otherwise. + """ + + shell = None + try: + from IPython import get_ipython + + shell = get_ipython().__class__.__name__ + finally: + return shell != "ZMQInteractiveShell" and getenv("DISABLE_INTERACTIVE_MODE") is None
+ + + +
+[docs] +def check_happy_path( + pipeline: Pipeline, + happy_path: Tuple[Tuple[Any, Any], ...], + # This optional argument is used for additional processing of candidate responses and reference responses + response_comparer: Callable[[Any, Any, Context], Optional[str]] = default_comparer, + printout_enable: bool = True, +): + """ + Running tutorial with provided pipeline for provided requests, comparing responses with correct expected responses. + In cases when additional processing of responses is needed (e.g. in case of response being an HTML string), + a special function (response comparer) is used. + + :param pipeline: The Pipeline instance, that will be used for checking. + :param happy_path: A tuple of (request, response) tuples, so-called happy path, + its requests are passed to pipeline and the pipeline responses are compared to its responses. + :param response_comparer: A special comparer function that accepts received response, true response and context; + it returns `None` is two responses are equal and transformed received response if they are different. + :param printout_enable: A flag that enables requests and responses fancy printing (to STDOUT). + """ + + ctx_id = uuid4() # get random ID for current context + for step_id, (request, reference_response) in enumerate(happy_path): + ctx = pipeline(request, ctx_id) + candidate_response = ctx.last_response + if printout_enable: + print(f"(user) >>> {repr(request)}") + print(f" (bot) <<< {repr(candidate_response)}") + parsed_response_with_deviation = response_comparer(candidate_response, reference_response, ctx) + if parsed_response_with_deviation is not None: + raise Exception( + f"\n\npipeline = {pipeline.info_dict}\n\n" + f"ctx = {ctx}\n\n" + f"step_id = {step_id}\n" + f"request = {repr(request)}\n" + f"candidate_response = {repr(parsed_response_with_deviation)}\n" + f"reference_response = {repr(reference_response)}\n" + "candidate_response != reference_response" + )
+ + + +
+[docs] +def run_interactive_mode(pipeline: Pipeline): # pragma: no cover + """ + Running tutorial with provided pipeline in interactive mode, just like with CLI messenger interface. + The dialog won't be stored anywhere, it will only be outputted to STDOUT. + + :param pipeline: The Pipeline instance, that will be used for running. + """ + + ctx_id = uuid4() # Random UID + print("Start a dialogue with the bot") + while True: + request = input(">>> ") + ctx = pipeline(request=Message(text=request), ctx_id=ctx_id) + print(f"<<< {repr(ctx.last_response)}")
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/testing/response_comparers.html b/_modules/dff/utils/testing/response_comparers.html new file mode 100644 index 0000000000..3a6ac7b210 --- /dev/null +++ b/_modules/dff/utils/testing/response_comparers.html @@ -0,0 +1,603 @@ + + + + + + + + + + dff.utils.testing.response_comparers — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.testing.response_comparers

+"""
+Response comparer
+-----------------
+This module defines function used to compare two response objects.
+"""
+from typing import Any, Optional
+
+from dff.script import Context, Message
+
+
+
+[docs] +def default_comparer(candidate: Message, reference: Message, _: Context) -> Optional[Any]: + """ + The default response comparer. Literally compares two response objects. + + :param candidate: The received (candidate) response. + :param reference: The true (reference) response. + :param _: Current Context (unused). + :return: `None` if two responses are equal or candidate response otherwise. + """ + return None if candidate == reference else candidate
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/testing/telegram.html b/_modules/dff/utils/testing/telegram.html new file mode 100644 index 0000000000..f6ae2449bb --- /dev/null +++ b/_modules/dff/utils/testing/telegram.html @@ -0,0 +1,885 @@ + + + + + + + + + + dff.utils.testing.telegram — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.testing.telegram

+"""
+Telegram testing utils
+----------------------
+This module defines functions used to test Telegram interface.
+"""
+from typing import List, Optional, cast, Tuple
+from contextlib import asynccontextmanager, nullcontext
+import logging
+import asyncio
+from tempfile import TemporaryDirectory
+from pathlib import Path
+from copy import deepcopy
+
+from telethon.tl.types import ReplyKeyboardHide
+from telethon import TelegramClient
+from telethon.types import User
+from telethon.custom import Message as TlMessage
+from telebot import types
+
+from dff.pipeline.pipeline.pipeline import Pipeline
+from dff.script.core.message import Message, Attachments, Attachment, Button, Location
+from dff.messengers.telegram.interface import PollingTelegramInterface
+from dff.messengers.telegram.message import TelegramMessage, TelegramUI, RemoveKeyboard, _ClickButton
+
+
+
+[docs] +def replace_click_button(happy_path): + """ + Replace all _ClickButton instances in `happy_path`. + This allows using :py:func:`~dff.utils.testing.common.check_happy_path` instead of + :py:meth:~dff.utils.testing.telegram.TelegramTesting.check_happy_path`. + + :return: A `happy_path` with all `_ClickButton` replaced with payload values of the buttons. + """ + result = deepcopy(happy_path) + for index in range(len(happy_path)): + user_request = happy_path[index][0] + if not isinstance(user_request, TelegramMessage): + continue + if isinstance(user_request.callback_query, _ClickButton): + callback_query = None + for _, bot_response in reversed(happy_path[:index]): + if isinstance(bot_response, TelegramMessage) and bot_response.ui is not None and callback_query is None: + callback_query = bot_response.ui.buttons[user_request.callback_query.button_index].payload + if callback_query is None: + raise RuntimeError("Bot response with buttons not found.") + result[index][0].callback_query = callback_query + return result
+ + + +
+[docs] +async def get_bot_user(client: TelegramClient, username: str): + async with client: + return await client.get_entity(username)
+ + + +
+[docs] +class TelegramTesting: # pragma: no cover + """ + Defines functions for testing. + + :param pipeline: + Pipeline with the telegram messenger interface. + Required for :py:meth:`~dff.utils.testing.telegram.TelegramTesting.send_and_check` and + :py:meth:`~dff.utils.testing.telegram.TelegramTesting.check_happy_path` with `run_bot=True` + :param api_credentials: + Telegram API id and hash. + Obtainable via https://core.telegram.org/api/obtaining_api_id. + :param session_file: + A `telethon` session file. + Obtainable by connecting to :py:class:`telethon.TelegramClient` and entering phone number and code. + :param client: + An alternative to passing `api_credentials` and `session_file`. + :param bot_username: + Either a link to the bot user or its handle. Used to determine whom to talk with as a client. + :param bot: + An alternative to passing `bot_username`. + Result of calling :py:func:`~dff.utils.testing.telegram.get_bot_user` with `bot_username` as parameter. + """ + + def __init__( + self, + pipeline: Pipeline, + api_credentials: Optional[Tuple[int, str]] = None, + session_file: Optional[str] = None, + client: Optional[TelegramClient] = None, + bot_username: Optional[str] = None, + bot: Optional[User] = None, + ): + if client is None: + if api_credentials is None or session_file is None: + raise RuntimeError("Pass either `client` or `api_credentials` and `session_file`.") + client = TelegramClient(session_file, *api_credentials) + self.client = client + """Telegram client (not bot). Needed to verify bot replies.""" + self.pipeline = pipeline + if bot is None: + if bot_username is None: + raise RuntimeError("Pass either `bot_username` or `bot`.") + bot = asyncio.run(get_bot_user(self.client, bot_username)) + self.bot = bot + """Bot user (to know whom to send messages to from client).""" + +
+[docs] + async def send_message(self, message: TelegramMessage, last_bot_messages: List[TlMessage]): + """ + Send a message from client to bot. + If the message contains `callback_query`, only press the button, ignore other fields. + + :param message: Message to send. + :param last_bot_messages: + The last bot response. Accepts a list because messages with multiple fields are split in telegram. + Can only contain one keyboard in the list. + Used to determine which button to press when message contains + :py:class:`~dff.messengers.telegram.message._ClickButton`. + """ + if message.callback_query is not None: + query = message.callback_query + if not isinstance(query, _ClickButton): + raise RuntimeError(f"Use `_ClickButton` during tests: {query}") + for bot_message in last_bot_messages: + if bot_message.buttons is not None: + await bot_message.click(i=query.button_index) + return None + if message.attachments is None or len(message.attachments.files) == 0: + return await self.client.send_message(self.bot, message.text) + else: + if len(message.attachments.files) == 1: + attachment = message.attachments.files[0] + files = attachment.source + else: + files = [file.source for file in message.attachments.files] + return await self.client.send_file(self.bot, files, caption=message.text)
+ + +
+[docs] + @staticmethod + async def parse_responses(responses: List[TlMessage], file_download_destination) -> Message: + """ + Convert a list of bot responses into a single message. + This function accepts a list because messages with multiple attachments are split. + + :param responses: A list of bot responses that are considered to be a single message. + :param file_download_destination: A directory to download sent media to. + """ + msg = TelegramMessage() + for response in responses: + if response.text and response.file is None: + if msg.text: + raise RuntimeError(f"Several messages with text:\n{msg.text}\n{response.text}") + msg.text = response.text or msg.text + if response.file is not None: + file = Path(file_download_destination) / (str(response.file.media.id) + response.file.ext) + await response.download_media(file=file) + if msg.attachments is None: + msg.attachments = Attachments() + msg.attachments.files.append( + Attachment(source=file, id=None, title=response.file.title or response.text or None) + ) + if response.buttons is not None: + buttons = [] + for row in response.buttons: + for button in row: + buttons.append( + Button( + source=button.url, + text=button.text, + payload=button.data, + ) + ) + if msg.ui is not None: + raise RuntimeError(f"Several messages with ui:\n{msg.ui}\n{TelegramUI(buttons=buttons)}") + msg.ui = TelegramUI(buttons=buttons) + if isinstance(response.reply_markup, ReplyKeyboardHide): + if msg.ui is not None: + raise RuntimeError(f"Several messages with ui:\n{msg.ui}\n{types.ReplyKeyboardRemove()}") + msg.ui = RemoveKeyboard() + if response.geo is not None: + location = Location(latitude=response.geo.lat, longitude=response.geo.long) + if msg.location is not None: + raise RuntimeError(f"Several messages with location:\n{msg.location}\n{location}") + msg.location = location + return msg
+ + +
+[docs] + @asynccontextmanager + async def run_bot_loop(self): + """A context manager that returns a function to run one polling loop of a messenger interface.""" + self.pipeline.messenger_interface.timeout = 2 + self.pipeline.messenger_interface.long_polling_timeout = 2 + await self.forget_previous_updates() + + yield lambda: self.pipeline.messenger_interface._polling_loop(self.pipeline._run_pipeline) + + self.pipeline.messenger_interface.forget_processed_updates()
+ + +
+[docs] + async def send_and_check(self, message: Message, file_download_destination=None): + """ + Send a message from a bot, receive it as client, verify it. + + :param message: Message to send and check. + :param file_download_destination: + Temporary directory (used to download sent files). + Defaults to :py:class:`tempfile.TemporaryDirectory`. + """ + await self.forget_previous_updates() + + async with self.client: + messenger_interface = cast(PollingTelegramInterface, self.pipeline.messenger_interface) + + messages = await self.client.get_messages(self.bot, limit=1) + if len(messages) == 0: + last_message_id = 0 + else: + last_message_id = messages[0].id + + messenger_interface.messenger.send_response((await self.client.get_me(input_peer=True)).user_id, message) + + await asyncio.sleep(3) + bot_messages = [ + x async for x in self.client.iter_messages(self.bot, min_id=last_message_id, from_user=self.bot) + ] # iter_messages is used instead of get_messages because get_messages requires bot min_id and max_id + + if file_download_destination is None: + fd_context = TemporaryDirectory() + else: + fd_context = nullcontext(file_download_destination) + + with fd_context as file_download_destination: + result = await self.parse_responses(bot_messages, file_download_destination) + + assert result == message
+ + +
+[docs] + async def forget_previous_updates(self): + messenger_interface = cast(PollingTelegramInterface, self.pipeline.messenger_interface) + messenger = messenger_interface.messenger + updates = messenger.get_updates(offset=messenger.last_update_id + 1, timeout=1, long_polling_timeout=1) + max_update_id = max([*map(lambda x: x.update_id, updates), -1]) + messenger.get_updates(offset=max_update_id + 1, timeout=1, long_polling_timeout=1)
+ + +
+[docs] + async def check_happy_path(self, happy_path, file_download_destination=None, run_bot: bool = True): + """ + Play out a dialogue with the bot. Check that the dialogue is correct. + + :param happy_path: Expected dialogue + :param file_download_destination: Temporary directory (used to download sent files) + :param run_bot: Whether a bot inside pipeline should be running (disable this to test non-async bots) + :return: + """ + if run_bot: + bot = self.run_bot_loop() + else: + + async def null(): + ... + + bot = nullcontext(null) + + if file_download_destination is None: + fd_context = TemporaryDirectory() + else: + fd_context = nullcontext(file_download_destination) + + async with self.client, bot as boot_loop: + with fd_context as file_download_destination: + bot_messages = [] + last_message = None + for request, response in happy_path: + logging.info(f"Sending request {request}") + user_message = await self.send_message(TelegramMessage.model_validate(request), bot_messages) + if user_message is not None: + last_message = user_message + logging.info("Request sent") + await boot_loop() + await asyncio.sleep(2) + logging.info("Extracting responses") + bot_messages = [ + x async for x in self.client.iter_messages(self.bot, min_id=last_message.id, from_user=self.bot) + ] + # iter_messages is used instead of get_messages because get_messages requires bot min_id and max_id + if len(bot_messages) > 0: + last_message = bot_messages[0] + logging.info("Got responses") + result = await self.parse_responses(bot_messages, file_download_destination) + assert result == TelegramMessage.model_validate(response)
+
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/dff/utils/turn_caching/singleton_turn_caching.html b/_modules/dff/utils/turn_caching/singleton_turn_caching.html new file mode 100644 index 0000000000..246fa5bbd6 --- /dev/null +++ b/_modules/dff/utils/turn_caching/singleton_turn_caching.html @@ -0,0 +1,638 @@ + + + + + + + + + + dff.utils.turn_caching.singleton_turn_caching — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + + + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for dff.utils.turn_caching.singleton_turn_caching

+"""
+Singleton Turn Caching
+----------------------
+This module contains functions for caching function results on each dialog turn.
+"""
+import functools
+from typing import Callable, List, Optional
+
+
+USED_CACHES: List[Callable] = list()
+"""Cache singleton, it is common for all actors and pipelines in current environment."""
+
+
+
+[docs] +def cache_clear(): + """ + Function for cache singleton clearing, it is called in the end of pipeline execution turn. + """ + for used_cache in USED_CACHES: + used_cache.cache_clear()
+ + + +
+[docs] +def lru_cache(maxsize: Optional[int] = None, typed: bool = False) -> Callable: + """ + Decorator function for caching function results in scripts. + Works like the standard :py:func:`~functools.lru_cache` function. + Caches are kept in a library-wide singleton and cleared in the end of each turn. + """ + + def decorator(func): + global USED_CACHES + + @functools.wraps(func) + @functools.lru_cache(maxsize=maxsize, typed=typed) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + USED_CACHES += [wrapper] + return wrapper + + return decorator
+ + + +
+[docs] +def cache(func): + """ + Decorator function for caching function results in scripts. + Works like the standard :py:func:`~functools.cache` function. + Caches are kept in a library-wide singleton and cleared in the end of each turn. + """ + return lru_cache(maxsize=None)(func)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000000..5618a69874 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + Overview: module code — DFF 0.6.4 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + + +
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/_sources/about_us.rst.txt b/_sources/about_us.rst.txt new file mode 100644 index 0000000000..4eacd29d12 --- /dev/null +++ b/_sources/about_us.rst.txt @@ -0,0 +1,8 @@ +About us +-------- + +Dialog Flow Framework is developed by the DFF development department of `DeepPavlov `_. + +Founder and leader of the project -- Denis Kuznetsov. + +Developers -- Roman Zlobin, Aleksandr Sergeev, Daniil Ignatiev, Aleksandr Sakharov. \ No newline at end of file diff --git a/_sources/apiref/dff.context_storages.database.rst.txt b/_sources/apiref/dff.context_storages.database.rst.txt new file mode 100644 index 0000000000..d066c5dd6d --- /dev/null +++ b/_sources/apiref/dff.context_storages.database.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/database + +.. automodule:: dff.context_storages.database + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.json.rst.txt b/_sources/apiref/dff.context_storages.json.rst.txt new file mode 100644 index 0000000000..b0503ccfe3 --- /dev/null +++ b/_sources/apiref/dff.context_storages.json.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/json + +.. automodule:: dff.context_storages.json + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.mongo.rst.txt b/_sources/apiref/dff.context_storages.mongo.rst.txt new file mode 100644 index 0000000000..57163c5fc2 --- /dev/null +++ b/_sources/apiref/dff.context_storages.mongo.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/mongo + +.. automodule:: dff.context_storages.mongo + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.pickle.rst.txt b/_sources/apiref/dff.context_storages.pickle.rst.txt new file mode 100644 index 0000000000..b4c3eaf643 --- /dev/null +++ b/_sources/apiref/dff.context_storages.pickle.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/pickle + +.. automodule:: dff.context_storages.pickle + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.protocol.rst.txt b/_sources/apiref/dff.context_storages.protocol.rst.txt new file mode 100644 index 0000000000..4a2d208384 --- /dev/null +++ b/_sources/apiref/dff.context_storages.protocol.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/protocol + +.. automodule:: dff.context_storages.protocol + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.redis.rst.txt b/_sources/apiref/dff.context_storages.redis.rst.txt new file mode 100644 index 0000000000..05aa567cc3 --- /dev/null +++ b/_sources/apiref/dff.context_storages.redis.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/redis + +.. automodule:: dff.context_storages.redis + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.shelve.rst.txt b/_sources/apiref/dff.context_storages.shelve.rst.txt new file mode 100644 index 0000000000..71452671e8 --- /dev/null +++ b/_sources/apiref/dff.context_storages.shelve.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/shelve + +.. automodule:: dff.context_storages.shelve + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.sql.rst.txt b/_sources/apiref/dff.context_storages.sql.rst.txt new file mode 100644 index 0000000000..7f5e0ccecc --- /dev/null +++ b/_sources/apiref/dff.context_storages.sql.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/sql + +.. automodule:: dff.context_storages.sql + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.context_storages.ydb.rst.txt b/_sources/apiref/dff.context_storages.ydb.rst.txt new file mode 100644 index 0000000000..a59a14aeb5 --- /dev/null +++ b/_sources/apiref/dff.context_storages.ydb.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/context_storages/ydb + +.. automodule:: dff.context_storages.ydb + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.common.interface.rst.txt b/_sources/apiref/dff.messengers.common.interface.rst.txt new file mode 100644 index 0000000000..cf4b9fd411 --- /dev/null +++ b/_sources/apiref/dff.messengers.common.interface.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/common/interface + +.. automodule:: dff.messengers.common.interface + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.common.types.rst.txt b/_sources/apiref/dff.messengers.common.types.rst.txt new file mode 100644 index 0000000000..9bc80fbcc5 --- /dev/null +++ b/_sources/apiref/dff.messengers.common.types.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/common/types + +.. automodule:: dff.messengers.common.types + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.telegram.interface.rst.txt b/_sources/apiref/dff.messengers.telegram.interface.rst.txt new file mode 100644 index 0000000000..3e4e0d5c76 --- /dev/null +++ b/_sources/apiref/dff.messengers.telegram.interface.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/telegram/interface + +.. automodule:: dff.messengers.telegram.interface + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.telegram.message.rst.txt b/_sources/apiref/dff.messengers.telegram.message.rst.txt new file mode 100644 index 0000000000..5d552fdec3 --- /dev/null +++ b/_sources/apiref/dff.messengers.telegram.message.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/telegram/message + +.. automodule:: dff.messengers.telegram.message + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.telegram.messenger.rst.txt b/_sources/apiref/dff.messengers.telegram.messenger.rst.txt new file mode 100644 index 0000000000..c752252bf2 --- /dev/null +++ b/_sources/apiref/dff.messengers.telegram.messenger.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/telegram/messenger + +.. automodule:: dff.messengers.telegram.messenger + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.messengers.telegram.utils.rst.txt b/_sources/apiref/dff.messengers.telegram.utils.rst.txt new file mode 100644 index 0000000000..e8cea54a8b --- /dev/null +++ b/_sources/apiref/dff.messengers.telegram.utils.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/messengers/telegram/utils + +.. automodule:: dff.messengers.telegram.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.conditions.rst.txt b/_sources/apiref/dff.pipeline.conditions.rst.txt new file mode 100644 index 0000000000..f8390c6a85 --- /dev/null +++ b/_sources/apiref/dff.pipeline.conditions.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/conditions + +.. automodule:: dff.pipeline.conditions + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.pipeline.actor.rst.txt b/_sources/apiref/dff.pipeline.pipeline.actor.rst.txt new file mode 100644 index 0000000000..ecafe83dfc --- /dev/null +++ b/_sources/apiref/dff.pipeline.pipeline.actor.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/pipeline/actor + +.. automodule:: dff.pipeline.pipeline.actor + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.pipeline.component.rst.txt b/_sources/apiref/dff.pipeline.pipeline.component.rst.txt new file mode 100644 index 0000000000..ef42951245 --- /dev/null +++ b/_sources/apiref/dff.pipeline.pipeline.component.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/pipeline/component + +.. automodule:: dff.pipeline.pipeline.component + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.pipeline.pipeline.rst.txt b/_sources/apiref/dff.pipeline.pipeline.pipeline.rst.txt new file mode 100644 index 0000000000..5bee8a72d2 --- /dev/null +++ b/_sources/apiref/dff.pipeline.pipeline.pipeline.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/pipeline/pipeline + +.. automodule:: dff.pipeline.pipeline.pipeline + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.pipeline.utils.rst.txt b/_sources/apiref/dff.pipeline.pipeline.utils.rst.txt new file mode 100644 index 0000000000..44dd020256 --- /dev/null +++ b/_sources/apiref/dff.pipeline.pipeline.utils.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/pipeline/utils + +.. automodule:: dff.pipeline.pipeline.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.service.extra.rst.txt b/_sources/apiref/dff.pipeline.service.extra.rst.txt new file mode 100644 index 0000000000..b8525d2ac2 --- /dev/null +++ b/_sources/apiref/dff.pipeline.service.extra.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/service/extra + +.. automodule:: dff.pipeline.service.extra + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.service.group.rst.txt b/_sources/apiref/dff.pipeline.service.group.rst.txt new file mode 100644 index 0000000000..c9813524e2 --- /dev/null +++ b/_sources/apiref/dff.pipeline.service.group.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/service/group + +.. automodule:: dff.pipeline.service.group + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.service.service.rst.txt b/_sources/apiref/dff.pipeline.service.service.rst.txt new file mode 100644 index 0000000000..673ecb4ff1 --- /dev/null +++ b/_sources/apiref/dff.pipeline.service.service.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/service/service + +.. automodule:: dff.pipeline.service.service + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.service.utils.rst.txt b/_sources/apiref/dff.pipeline.service.utils.rst.txt new file mode 100644 index 0000000000..0fda2b8eef --- /dev/null +++ b/_sources/apiref/dff.pipeline.service.utils.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/service/utils + +.. automodule:: dff.pipeline.service.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.pipeline.types.rst.txt b/_sources/apiref/dff.pipeline.types.rst.txt new file mode 100644 index 0000000000..3658cd98fb --- /dev/null +++ b/_sources/apiref/dff.pipeline.types.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/pipeline/types + +.. automodule:: dff.pipeline.types + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.conditions.std_conditions.rst.txt b/_sources/apiref/dff.script.conditions.std_conditions.rst.txt new file mode 100644 index 0000000000..9452e07b1a --- /dev/null +++ b/_sources/apiref/dff.script.conditions.std_conditions.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/conditions/std_conditions + +.. automodule:: dff.script.conditions.std_conditions + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.context.rst.txt b/_sources/apiref/dff.script.core.context.rst.txt new file mode 100644 index 0000000000..8f40706bca --- /dev/null +++ b/_sources/apiref/dff.script.core.context.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/context + +.. automodule:: dff.script.core.context + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.keywords.rst.txt b/_sources/apiref/dff.script.core.keywords.rst.txt new file mode 100644 index 0000000000..6a66bbd854 --- /dev/null +++ b/_sources/apiref/dff.script.core.keywords.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/keywords + +.. automodule:: dff.script.core.keywords + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.message.rst.txt b/_sources/apiref/dff.script.core.message.rst.txt new file mode 100644 index 0000000000..16e406c701 --- /dev/null +++ b/_sources/apiref/dff.script.core.message.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/message + +.. automodule:: dff.script.core.message + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.normalization.rst.txt b/_sources/apiref/dff.script.core.normalization.rst.txt new file mode 100644 index 0000000000..5ae5f028a0 --- /dev/null +++ b/_sources/apiref/dff.script.core.normalization.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/normalization + +.. automodule:: dff.script.core.normalization + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.script.rst.txt b/_sources/apiref/dff.script.core.script.rst.txt new file mode 100644 index 0000000000..d55b17f08e --- /dev/null +++ b/_sources/apiref/dff.script.core.script.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/script + +.. automodule:: dff.script.core.script + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.core.types.rst.txt b/_sources/apiref/dff.script.core.types.rst.txt new file mode 100644 index 0000000000..e86e0857da --- /dev/null +++ b/_sources/apiref/dff.script.core.types.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/core/types + +.. automodule:: dff.script.core.types + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.extras.conditions.rst.txt b/_sources/apiref/dff.script.extras.conditions.rst.txt new file mode 100644 index 0000000000..6ebb47e585 --- /dev/null +++ b/_sources/apiref/dff.script.extras.conditions.rst.txt @@ -0,0 +1,12 @@ +:source_name: dff/script/extras/conditions + +dff.script.extras.conditions package +==================================== + +Module contents +--------------- + +.. automodule:: dff.script.extras.conditions + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.extras.slots.rst.txt b/_sources/apiref/dff.script.extras.slots.rst.txt new file mode 100644 index 0000000000..205fe4174b --- /dev/null +++ b/_sources/apiref/dff.script.extras.slots.rst.txt @@ -0,0 +1,12 @@ +:source_name: dff/script/extras/slots + +dff.script.extras.slots package +=============================== + +Module contents +--------------- + +.. automodule:: dff.script.extras.slots + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.labels.std_labels.rst.txt b/_sources/apiref/dff.script.labels.std_labels.rst.txt new file mode 100644 index 0000000000..db10a608c2 --- /dev/null +++ b/_sources/apiref/dff.script.labels.std_labels.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/labels/std_labels + +.. automodule:: dff.script.labels.std_labels + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.script.responses.std_responses.rst.txt b/_sources/apiref/dff.script.responses.std_responses.rst.txt new file mode 100644 index 0000000000..bc8a8e8b8c --- /dev/null +++ b/_sources/apiref/dff.script.responses.std_responses.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/script/responses/std_responses + +.. automodule:: dff.script.responses.std_responses + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.stats.cli.rst.txt b/_sources/apiref/dff.stats.cli.rst.txt new file mode 100644 index 0000000000..d23493194a --- /dev/null +++ b/_sources/apiref/dff.stats.cli.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/stats/cli + +.. automodule:: dff.stats.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.stats.default_extractors.rst.txt b/_sources/apiref/dff.stats.default_extractors.rst.txt new file mode 100644 index 0000000000..a0ee6e2ba9 --- /dev/null +++ b/_sources/apiref/dff.stats.default_extractors.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/stats/default_extractors + +.. automodule:: dff.stats.default_extractors + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.stats.instrumentor.rst.txt b/_sources/apiref/dff.stats.instrumentor.rst.txt new file mode 100644 index 0000000000..bef1ed24c7 --- /dev/null +++ b/_sources/apiref/dff.stats.instrumentor.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/stats/instrumentor + +.. automodule:: dff.stats.instrumentor + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.stats.utils.rst.txt b/_sources/apiref/dff.stats.utils.rst.txt new file mode 100644 index 0000000000..e3094d7b26 --- /dev/null +++ b/_sources/apiref/dff.stats.utils.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/stats/utils + +.. automodule:: dff.stats.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.db_benchmark.basic_config.rst.txt b/_sources/apiref/dff.utils.db_benchmark.basic_config.rst.txt new file mode 100644 index 0000000000..204b26b546 --- /dev/null +++ b/_sources/apiref/dff.utils.db_benchmark.basic_config.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/db_benchmark/basic_config + +.. automodule:: dff.utils.db_benchmark.basic_config + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.db_benchmark.benchmark.rst.txt b/_sources/apiref/dff.utils.db_benchmark.benchmark.rst.txt new file mode 100644 index 0000000000..aad30022e9 --- /dev/null +++ b/_sources/apiref/dff.utils.db_benchmark.benchmark.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/db_benchmark/benchmark + +.. automodule:: dff.utils.db_benchmark.benchmark + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.db_benchmark.report.rst.txt b/_sources/apiref/dff.utils.db_benchmark.report.rst.txt new file mode 100644 index 0000000000..7d62dabd81 --- /dev/null +++ b/_sources/apiref/dff.utils.db_benchmark.report.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/db_benchmark/report + +.. automodule:: dff.utils.db_benchmark.report + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.testing.cleanup_db.rst.txt b/_sources/apiref/dff.utils.testing.cleanup_db.rst.txt new file mode 100644 index 0000000000..811aea1211 --- /dev/null +++ b/_sources/apiref/dff.utils.testing.cleanup_db.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/testing/cleanup_db + +.. automodule:: dff.utils.testing.cleanup_db + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.testing.common.rst.txt b/_sources/apiref/dff.utils.testing.common.rst.txt new file mode 100644 index 0000000000..346f64250a --- /dev/null +++ b/_sources/apiref/dff.utils.testing.common.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/testing/common + +.. automodule:: dff.utils.testing.common + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.testing.response_comparers.rst.txt b/_sources/apiref/dff.utils.testing.response_comparers.rst.txt new file mode 100644 index 0000000000..505cac3a52 --- /dev/null +++ b/_sources/apiref/dff.utils.testing.response_comparers.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/testing/response_comparers + +.. automodule:: dff.utils.testing.response_comparers + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.testing.telegram.rst.txt b/_sources/apiref/dff.utils.testing.telegram.rst.txt new file mode 100644 index 0000000000..b7efe86359 --- /dev/null +++ b/_sources/apiref/dff.utils.testing.telegram.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/testing/telegram + +.. automodule:: dff.utils.testing.telegram + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.testing.toy_script.rst.txt b/_sources/apiref/dff.utils.testing.toy_script.rst.txt new file mode 100644 index 0000000000..4e781ac81a --- /dev/null +++ b/_sources/apiref/dff.utils.testing.toy_script.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/testing/toy_script + +.. automodule:: dff.utils.testing.toy_script + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/dff.utils.turn_caching.singleton_turn_caching.rst.txt b/_sources/apiref/dff.utils.turn_caching.singleton_turn_caching.rst.txt new file mode 100644 index 0000000000..8fa7968f27 --- /dev/null +++ b/_sources/apiref/dff.utils.turn_caching.singleton_turn_caching.rst.txt @@ -0,0 +1,6 @@ +:source_name: dff/utils/turn_caching/singleton_turn_caching + +.. automodule:: dff.utils.turn_caching.singleton_turn_caching + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/apiref/index_caching.rst.txt b/_sources/apiref/index_caching.rst.txt new file mode 100644 index 0000000000..3a2e87c87d --- /dev/null +++ b/_sources/apiref/index_caching.rst.txt @@ -0,0 +1,11 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Caching +======= + +.. autosummary:: + :toctree: + + dff.utils.turn_caching.singleton_turn_caching diff --git a/_sources/apiref/index_context_storages.rst.txt b/_sources/apiref/index_context_storages.rst.txt new file mode 100644 index 0000000000..63ab43d0ee --- /dev/null +++ b/_sources/apiref/index_context_storages.rst.txt @@ -0,0 +1,19 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Context Storages +================ + +.. autosummary:: + :toctree: + + dff.context_storages.protocol + dff.context_storages.database + dff.context_storages.redis + dff.context_storages.shelve + dff.context_storages.pickle + dff.context_storages.sql + dff.context_storages.mongo + dff.context_storages.json + dff.context_storages.ydb diff --git a/_sources/apiref/index_db_benchmark.rst.txt b/_sources/apiref/index_db_benchmark.rst.txt new file mode 100644 index 0000000000..9dd566453d --- /dev/null +++ b/_sources/apiref/index_db_benchmark.rst.txt @@ -0,0 +1,13 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +DB Benchmark +============ + +.. autosummary:: + :toctree: + + dff.utils.db_benchmark.benchmark + dff.utils.db_benchmark.basic_config + dff.utils.db_benchmark.report diff --git a/_sources/apiref/index_messenger_interfaces.rst.txt b/_sources/apiref/index_messenger_interfaces.rst.txt new file mode 100644 index 0000000000..a6c7d1e9d8 --- /dev/null +++ b/_sources/apiref/index_messenger_interfaces.rst.txt @@ -0,0 +1,16 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Messenger Interfaces +==================== + +.. autosummary:: + :toctree: + + dff.messengers.telegram.utils + dff.messengers.telegram.messenger + dff.messengers.telegram.interface + dff.messengers.common.interface + dff.messengers.telegram.message + dff.messengers.common.types diff --git a/_sources/apiref/index_pipeline.rst.txt b/_sources/apiref/index_pipeline.rst.txt new file mode 100644 index 0000000000..eb486023b5 --- /dev/null +++ b/_sources/apiref/index_pipeline.rst.txt @@ -0,0 +1,20 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Pipeline +======== + +.. autosummary:: + :toctree: + + dff.pipeline.service.utils + dff.pipeline.service.extra + dff.pipeline.pipeline.utils + dff.pipeline.pipeline.component + dff.pipeline.service.group + dff.pipeline.service.service + dff.pipeline.pipeline.pipeline + dff.pipeline.pipeline.actor + dff.pipeline.types + dff.pipeline.conditions diff --git a/_sources/apiref/index_script.rst.txt b/_sources/apiref/index_script.rst.txt new file mode 100644 index 0000000000..6f1f879fb0 --- /dev/null +++ b/_sources/apiref/index_script.rst.txt @@ -0,0 +1,21 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Script +====== + +.. autosummary:: + :toctree: + + dff.script.conditions.std_conditions + dff.script.responses.std_responses + dff.script.core.types + dff.script.extras.conditions + dff.script.core.script + dff.script.labels.std_labels + dff.script.core.normalization + dff.script.core.message + dff.script.extras.slots + dff.script.core.keywords + dff.script.core.context diff --git a/_sources/apiref/index_stats.rst.txt b/_sources/apiref/index_stats.rst.txt new file mode 100644 index 0000000000..6658f73287 --- /dev/null +++ b/_sources/apiref/index_stats.rst.txt @@ -0,0 +1,14 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Stats +===== + +.. autosummary:: + :toctree: + + dff.stats.cli + dff.stats.default_extractors + dff.stats.instrumentor + dff.stats.utils diff --git a/_sources/apiref/index_testing_utils.rst.txt b/_sources/apiref/index_testing_utils.rst.txt new file mode 100644 index 0000000000..3ad7a49008 --- /dev/null +++ b/_sources/apiref/index_testing_utils.rst.txt @@ -0,0 +1,15 @@ +:orphan: + +.. This is an auto-generated RST file representing documentation source directory structure + +Testing Utils +============= + +.. autosummary:: + :toctree: + + dff.utils.testing.toy_script + dff.utils.testing.cleanup_db + dff.utils.testing.common + dff.utils.testing.telegram + dff.utils.testing.response_comparers diff --git a/_sources/community.rst.txt b/_sources/community.rst.txt new file mode 100644 index 0000000000..3aae1a75f4 --- /dev/null +++ b/_sources/community.rst.txt @@ -0,0 +1,21 @@ +Community +--------- + +This section provides links to different platforms where users of DFF can ask questions, +share their experiences, report issues, and communicate with the DFF development team and other DFF users. + +Please take a short survey about DFF: +`Google Form `_. +This will allow us to make it better. + +`DeepPavlov Forum `_ is designed to discuss various aspects of DeepPavlov, +which includes the DFF. + +`Telegram `_ is a group chat where DFF users can ask questions and +get help from the community. + +`GitHub Issues `_ is a platform where users +can report issues, suggest features, and track the progress of DFF development. + +`Stack Overflow `_ is a platform where DFF users can ask +technical questions and get answers from the community. \ No newline at end of file diff --git a/_sources/development.rst.txt b/_sources/development.rst.txt new file mode 100644 index 0000000000..19163a38de --- /dev/null +++ b/_sources/development.rst.txt @@ -0,0 +1,23 @@ +Development +----------- + +Contribution +~~~~~~~~~~~~~~~ + +`Contribution rules `_ provide +guidelines and rules for contributing to the Dialog Flow Framework project. It includes information on +how to contribute code to the project, manage your workflow, use tests, and so on. + +Project roadmap +~~~~~~~~~~~~~~~ + +`Project roadmap `_ +outlines the future development plans for DFF, including new features and enhancements +that are planned for upcoming releases. + +Release notes +~~~~~~~~~~~~~ + +`Release notes `_ +contain information about the latest releases of DFF, including new features, improvements, and bug fixes. + diff --git a/_sources/examples.rst.txt b/_sources/examples.rst.txt new file mode 100644 index 0000000000..6c5854dbc4 --- /dev/null +++ b/_sources/examples.rst.txt @@ -0,0 +1,4 @@ +Examples +-------- + +Examples are available in this `repository `_. diff --git a/_sources/get_started.rst.txt b/_sources/get_started.rst.txt new file mode 100644 index 0000000000..b0749793ee --- /dev/null +++ b/_sources/get_started.rst.txt @@ -0,0 +1,74 @@ +Getting started +--------------- + +Installation +~~~~~~~~~~~~ + +DFF can be easily installed on your system using the ``pip`` package manager: + +.. code-block:: console + + pip install dff + +This framework is compatible with Python 3.8 and newer versions. + +The above command will set the minimum dependencies to start working with DFF. +The installation process allows the user to choose from different packages based on their dependencies, which are: + +.. code-block:: console + + pip install dff[json] # dependencies for using JSON + pip install dff[pickle] # dependencies for using Pickle + pip install dff[redis] # dependencies for using Redis + pip install dff[mongodb] # dependencies for using MongoDB + pip install dff[mysql] # dependencies for using MySQL + pip install dff[postgresql] # dependencies for using PostgreSQL + pip install dff[sqlite] # dependencies for using SQLite + pip install dff[ydb] # dependencies for using Yandex Database + pip install dff[telegram] # dependencies for using Telegram + pip install dff[benchmark] # dependencies for benchmarking + +For example, if you are going to use one of the database backends, +you can specify the corresponding requirements yourself. + +Additionally, you also have the option to download the source code directly from the +`GitHub `_ repository using the commands: + +.. code-block:: console + + git clone https://github.com/deeppavlov/dialog_flow_framework.git + cd dialog_flow_framework + +Once you are in the directory, you can run the command ``make venv`` to set up all the necessary requirements for the library. +If you need to update the requirements, use the command ``make clean`` to remove `venv` first. + +Key concepts +~~~~~~~~~~~~ + +DFF is a powerful tool for creating conversational services. +It allows developers to easily write and manage dialog systems by defining a special +dialog graph that describes the behavior of the service. +DFF offers a specialized language (DSL) for quickly writing dialog graphs, +making it easy for developers to create chatbots for a wide +range of applications, such as social networks, call centers, websites, personal assistants, etc. + +DFF has several important concepts: + +**Script**: First of all, to create a dialog agent it is necessary +to create a dialog :py:class:`~dff.script.core.script.Script`. +A dialog `script` is a dictionary, where keys correspond to different `flows`. +A script can contain multiple scripts, which are flows too, what is needed in order to divide +a dialog into sub-dialogs and process them separately. + +**Flow**: As mentioned above, the dialog is divided into flows. +Each flow represent a sub-dialog corresponding to the discussion of a particular topic. +Each flow is also a dictionary, where the keys are the `nodes`. + +**Node**: A `node` is the smallest unit of a dialog flow, and it contains the bot's response +to a user's input as well as a `condition` that determines +the `transition` to another node, whether it's within the current or another flow. + +**Keywords**: DFF uses several special `keywords`. These keywords are the keys in the dictionaries inside the script. +The most important for using the framework are `RESPONSE` and `TRANSITIONS` keywords. +The first one corresponds to the response that the bot will send to the user from the current node. +The second corresponds to the transition conditions from the current node to other nodes. \ No newline at end of file diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 0000000000..42b71d3f36 --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,97 @@ +.. Dialog Flow Framework documentation master file, created by + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Dialog Flow Framework +===================== + +*Date*: |today| *Version*: |version| + +.. image:: https://pepy.tech/badge/dff + :alt: Number of downloads + :target: https://pypi.org/project/dff/ + +.. image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-green.svg + :alt: Supported python versions + +The Dialog Flow Framework (DFF) is an open-source, `Apache 2.0 `_-licensed library +that was developed specifically for creating dialog systems. DFF provides a comprehensive set of tools and resources, +targeting a wide range of applications, including chatbots, virtual assistants, and other interactive systems. +It allows developers to easily create and manage complex dialog flows, integrate with natural language processing (NLP) tools, +and handle user input in a flexible and efficient manner. Additionally, the framework is highly customizable, +allowing developers to easily adapt it to their specific needs and requirements. + +DFF documentation includes the following sections: + +:doc:`Getting started <./get_started>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Essential information about installing and using the library +aimed at beginners can be found in the ``Getting started`` part +of the documentation. This section also introduces the basic terms +that form the principles of the framework. +For deeper understanding of the API, consult the documentation sections described below. + +:doc:`User guides <./user_guides>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``User guides`` section provides comprehensive human-readable explanations +of how your conversational service should be set up and function. +It specifically highlights the aspects that are not covered by the API reference, +e.g. deployment, optimization techniques, etc. + +:doc:`Tutorials <./tutorials>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most capabilities of DFF can also be explored in the ``Tutorials`` +section. These interactive files dynamically showcase how different +modules and classes of the framework interact. + +:doc:`Examples <./examples>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Links to demonsration projects that leverage the library +integrating it with external services and models and serve as examples +can be found in this section. + +:doc:`API reference <./reference>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The API reference contains documentation for classes and abstractions +used in the library which can be used to determine the exact typing +and behavior of all the functions involved. + +:doc:`Development <./development>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Development`` section shows the library's current development status and specifies the contribution rules. + +:doc:`Community <./community>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Community`` section links you to useful resources where you can find supplemental information +about the framework and ask questions. + +:doc:`About us <./about_us>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can get more info about the development team in the ``About us`` section. + +.. toctree:: + :hidden: + :maxdepth: 1 + + get_started + user_guides + tutorials + examples + reference + development + community + about_us + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` diff --git a/_sources/reference.rst.txt b/_sources/reference.rst.txt new file mode 100644 index 0000000000..467c763a2e --- /dev/null +++ b/_sources/reference.rst.txt @@ -0,0 +1,9 @@ +API reference +------------- + +.. toctree:: + :name: reference + :glob: + :maxdepth: 1 + + apiref/index_* diff --git a/_sources/tutorials.rst.txt b/_sources/tutorials.rst.txt new file mode 100644 index 0000000000..ba9b2e0986 --- /dev/null +++ b/_sources/tutorials.rst.txt @@ -0,0 +1,30 @@ +Tutorials +--------- +Tutorials page is a collection of instructional materials designed to help developers learn +how to use DFF to build conversational agents. The tutorials cover a range of topics, +from getting started with DFF to more advanced topics such as integrating external APIs. +Each tutorial includes detailed explanations and code examples. Tutorials cover different aspects +of the framework and are organized into sections. + +The Context Storages section describes how to use context storages in DFF. +The Messengers section covers how to use the Telegram messenger with DFF. +The Pipeline section teaches the basics of the pipeline concept, how to use pre- and postprocessors, +asynchronous groups and services, custom messenger interfaces, and extra handlers and extensions. +The Script section covers the basics of the script concept, including conditions, responses, transitions, +and serialization. It also includes tutorials on pre-response and pre-transitions processing. +Finally, the Utils section covers the cache and LRU cache utilities in DFF. + +The main difference between Tutorials and Examples is that Tutorials typically show how to implement +a specific feature or solve a particular problem, whereas Examples provide a more +comprehensive overview of how to build a complete application. + +| To understand the basics of DFF, read the following tutorials: +| 1) Script / Core / 1. Basics +| 2) Script / Core / 2. Conditions +| 3) Pipeline / 1. Basics + +.. toctree:: + :name: tutorials + :glob: + + tutorials/index_* diff --git a/_sources/tutorials/index_context_storages.rst.txt b/_sources/tutorials/index_context_storages.rst.txt new file mode 100644 index 0000000000..55d72b14b4 --- /dev/null +++ b/_sources/tutorials/index_context_storages.rst.txt @@ -0,0 +1,16 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Context Storages +================ + +.. nbgallery:: + tutorials.context_storages.1_basics.py + tutorials.context_storages.2_postgresql.py + tutorials.context_storages.3_mongodb.py + tutorials.context_storages.4_redis.py + tutorials.context_storages.5_mysql.py + tutorials.context_storages.6_sqlite.py + tutorials.context_storages.7_yandex_database.py + tutorials.context_storages.8_db_benchmarking.py diff --git a/_sources/tutorials/index_interfaces.rst.txt b/_sources/tutorials/index_interfaces.rst.txt new file mode 100644 index 0000000000..b2daaf6db8 --- /dev/null +++ b/_sources/tutorials/index_interfaces.rst.txt @@ -0,0 +1,28 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Interfaces +========== + +Telegram +-------- + +.. nbgallery:: + tutorials.messengers.telegram.1_basic.py + tutorials.messengers.telegram.2_buttons.py + tutorials.messengers.telegram.3_buttons_with_callback.py + tutorials.messengers.telegram.4_conditions.py + tutorials.messengers.telegram.5_conditions_with_media.py + tutorials.messengers.telegram.6_conditions_extras.py + tutorials.messengers.telegram.7_polling_setup.py + tutorials.messengers.telegram.8_webhook_setup.py + +Web API +------- + +.. nbgallery:: + tutorials.messengers.web_api_interface.1_fastapi.py + tutorials.messengers.web_api_interface.2_websocket_chat.py + tutorials.messengers.web_api_interface.3_load_testing_with_locust.py + tutorials.messengers.web_api_interface.4_streamlit_chat.py diff --git a/_sources/tutorials/index_pipeline.rst.txt b/_sources/tutorials/index_pipeline.rst.txt new file mode 100644 index 0000000000..87e65577a5 --- /dev/null +++ b/_sources/tutorials/index_pipeline.rst.txt @@ -0,0 +1,19 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Pipeline +======== + +.. nbgallery:: + tutorials.pipeline.1_basics.py + tutorials.pipeline.2_pre_and_post_processors.py + tutorials.pipeline.3_pipeline_dict_with_services_basic.py + tutorials.pipeline.3_pipeline_dict_with_services_full.py + tutorials.pipeline.4_groups_and_conditions_basic.py + tutorials.pipeline.4_groups_and_conditions_full.py + tutorials.pipeline.5_asynchronous_groups_and_services_basic.py + tutorials.pipeline.5_asynchronous_groups_and_services_full.py + tutorials.pipeline.6_extra_handlers_basic.py + tutorials.pipeline.6_extra_handlers_full.py + tutorials.pipeline.7_extra_handlers_and_extensions.py diff --git a/_sources/tutorials/index_script.rst.txt b/_sources/tutorials/index_script.rst.txt new file mode 100644 index 0000000000..7a7eec6a31 --- /dev/null +++ b/_sources/tutorials/index_script.rst.txt @@ -0,0 +1,29 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Script +====== + +Core +---- + +.. nbgallery:: + tutorials.script.core.1_basics.py + tutorials.script.core.2_conditions.py + tutorials.script.core.3_responses.py + tutorials.script.core.4_transitions.py + tutorials.script.core.5_global_transitions.py + tutorials.script.core.6_context_serialization.py + tutorials.script.core.7_pre_response_processing.py + tutorials.script.core.8_misc.py + tutorials.script.core.9_pre_transitions_processing.py + +Responses +--------- + +.. nbgallery:: + tutorials.script.responses.1_basics.py + tutorials.script.responses.2_buttons.py + tutorials.script.responses.3_media.py + tutorials.script.responses.4_multi_message.py diff --git a/_sources/tutorials/index_stats.rst.txt b/_sources/tutorials/index_stats.rst.txt new file mode 100644 index 0000000000..64fa941200 --- /dev/null +++ b/_sources/tutorials/index_stats.rst.txt @@ -0,0 +1,10 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Stats +===== + +.. nbgallery:: + tutorials.stats.1_extractor_functions.py + tutorials.stats.2_pipeline_integration.py diff --git a/_sources/tutorials/index_utils.rst.txt b/_sources/tutorials/index_utils.rst.txt new file mode 100644 index 0000000000..56e2a7a3a2 --- /dev/null +++ b/_sources/tutorials/index_utils.rst.txt @@ -0,0 +1,10 @@ +:orphan: + +.. This is an auto-generated RST index file representing tutorials directory structure + +Utils +===== + +.. nbgallery:: + tutorials.utils.1_cache.py + tutorials.utils.2_lru_cache.py diff --git a/_sources/tutorials/tutorials.context_storages.1_basics.py.txt b/_sources/tutorials/tutorials.context_storages.1_basics.py.txt new file mode 100644 index 0000000000..449f93ef27 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.1_basics.py.txt @@ -0,0 +1,42 @@ +# %% [markdown] +""" +# 1. Basics + +The following tutorial shows the basic use of the database connection. + +See %mddoclink(api,context_storages.database,context_storage_factory) function +for creating a context storage by path. + +In this example JSON file is used as a storage. +""" + +# %pip install dff[json,pickle] + +# %% +import pathlib + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + +pathlib.Path("dbs").mkdir(exist_ok=True) +db = context_storage_factory("json://dbs/file.json") +# db = context_storage_factory("pickle://dbs/file.pkl") +# db = context_storage_factory("shelve://dbs/file") + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + # a function for automatic tutorial running (testing) with HAPPY_PATH + + # This runs tutorial in interactive mode if not in IPython env + # and if `DISABLE_INTERACTIVE_MODE` is not set + if is_interactive_mode(): + run_interactive_mode(pipeline) # This runs tutorial in interactive mode diff --git a/_sources/tutorials/tutorials.context_storages.2_postgresql.py.txt b/_sources/tutorials/tutorials.context_storages.2_postgresql.py.txt new file mode 100644 index 0000000000..4a92685755 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.2_postgresql.py.txt @@ -0,0 +1,47 @@ +# %% [markdown] +""" +# 2. PostgreSQL + +This is a tutorial on using PostgreSQL. + +See %mddoclink(api,context_storages.sql,SQLContextStorage) class +for storing your users' contexts in SQL databases. + +DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/) +and [asyncpg](https://magicstack.github.io/asyncpg/current/) +libraries for asynchronous access to PostgreSQL DB. +""" + +# %pip install dff[postgresql] + +# %% +import os + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +db_uri = "postgresql+asyncpg://{}:{}@localhost:5432/{}".format( + os.environ["POSTGRES_USERNAME"], + os.environ["POSTGRES_PASSWORD"], + os.environ["POSTGRES_DB"], +) +db = context_storage_factory(db_uri) + + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.3_mongodb.py.txt b/_sources/tutorials/tutorials.context_storages.3_mongodb.py.txt new file mode 100644 index 0000000000..f55ae3fbb7 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.3_mongodb.py.txt @@ -0,0 +1,45 @@ +# %% [markdown] +""" +# 3. MongoDB + +This is a tutorial on using MongoDB. + +See %mddoclink(api,context_storages.mongo,MongoContextStorage) class +for storing you users' contexts in Mongo database. + +DFF uses [motor](https://motor.readthedocs.io/en/stable/) +library for asynchronous access to MongoDB. +""" + +# %pip install dff[mongodb] + +# %% +import os + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +db_uri = "mongodb://{}:{}@localhost:27017/{}".format( + os.environ["MONGO_INITDB_ROOT_USERNAME"], + os.environ["MONGO_INITDB_ROOT_PASSWORD"], + os.environ["MONGO_INITDB_ROOT_USERNAME"], +) +db = context_storage_factory(db_uri) + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.4_redis.py.txt b/_sources/tutorials/tutorials.context_storages.4_redis.py.txt new file mode 100644 index 0000000000..918b5d1276 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.4_redis.py.txt @@ -0,0 +1,44 @@ +# %% [markdown] +""" +# 4. Redis + +This is a tutorial on using Redis. + +See %mddoclink(api,context_storages.redis,RedisContextStorage) class +for storing you users' contexts in Redis database. + +DFF uses [redis.asyncio](https://redis.readthedocs.io/en/latest/) +library for asynchronous access to Redis DB. +""" + +# %pip install dff[redis] + +# %% +import os + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +db_uri = "redis://{}:{}@localhost:6379/{}".format( + "", os.environ["REDIS_PASSWORD"], "0" +) +db = context_storage_factory(db_uri) + + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.5_mysql.py.txt b/_sources/tutorials/tutorials.context_storages.5_mysql.py.txt new file mode 100644 index 0000000000..aed04712ae --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.5_mysql.py.txt @@ -0,0 +1,47 @@ +# %% [markdown] +""" +# 5. MySQL + +This is a tutorial on using MySQL. + +See %mddoclink(api,context_storages.sql,SQLContextStorage) class +for storing you users' contexts in SQL databases. + +DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/) +and [asyncmy](https://github.com/long2ice/asyncmy) +libraries for asynchronous access to MySQL DB. +""" + +# %pip install dff[mysql] + +# %% +import os + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +db_uri = "mysql+asyncmy://{}:{}@localhost:3307/{}".format( + os.environ["MYSQL_USERNAME"], + os.environ["MYSQL_PASSWORD"], + os.environ["MYSQL_DATABASE"], +) +db = context_storage_factory(db_uri) + + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.6_sqlite.py.txt b/_sources/tutorials/tutorials.context_storages.6_sqlite.py.txt new file mode 100644 index 0000000000..8d7e0b53b4 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.6_sqlite.py.txt @@ -0,0 +1,51 @@ +# %% [markdown] +""" +# 6. SQLite + +This is a tutorial on using SQLite. + +See %mddoclink(api,context_storages.sql,SQLContextStorage) class +for storing you users' contexts in SQL databases. + +DFF uses [sqlalchemy](https://docs.sqlalchemy.org/en/20/) +and [aiosqlite](https://readthedocs.org/projects/aiosqlite/) +libraries for asynchronous access to SQLite DB. + +Note that protocol separator for windows differs from one for linux. +""" + +# %pip install dff[sqlite] + +# %% +import pathlib +from platform import system + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +pathlib.Path("dbs").mkdir(exist_ok=True) +db_file = pathlib.Path("dbs/sqlite.db") +db_file.touch(exist_ok=True) + +separator = "///" if system() == "Windows" else "////" +db_uri = f"sqlite+aiosqlite:{separator}{db_file.absolute()}" +db = context_storage_factory(db_uri) + + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.7_yandex_database.py.txt b/_sources/tutorials/tutorials.context_storages.7_yandex_database.py.txt new file mode 100644 index 0000000000..2474900416 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.7_yandex_database.py.txt @@ -0,0 +1,52 @@ +# %% [markdown] +""" +# 7. Yandex DataBase + +This is a tutorial on how to use Yandex DataBase. + +See %mddoclink(api,context_storages.ydb,YDBContextStorage) class +for storing you users' contexts in Yandex database. + +DFF uses [ydb.aio](https://ydb.tech/en/docs/) +library for asynchronous access to Yandex DB. +""" + +# %pip install dff[ydb] + +# %% +import os + +from dff.context_storages import context_storage_factory + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + run_interactive_mode, + is_interactive_mode, +) +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH + + +# %% +# ##### Connecting to yandex cloud +# https://github.com/zinal/ydb-python-sdk/blob/ex_basic-example_p1/examples/basic_example_v1/README.md +# export YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS=$HOME/key-ydb-sa-0.json +# export YDB_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135 +# export YDB_DATABASE=/ru-central1/qwertyuiopasdfgh/123456789qwertyui +# ##### or use local-ydb with variables from .env_file +# db_uri="grpc://localhost:2136/local" + +db_uri = "{}{}".format( + os.environ["YDB_ENDPOINT"], + os.environ["YDB_DATABASE"], +) +db = context_storage_factory(db_uri) + +pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.context_storages.8_db_benchmarking.py.txt b/_sources/tutorials/tutorials.context_storages.8_db_benchmarking.py.txt new file mode 100644 index 0000000000..1b56414b41 --- /dev/null +++ b/_sources/tutorials/tutorials.context_storages.8_db_benchmarking.py.txt @@ -0,0 +1,241 @@ +# %% [markdown] +""" +# 8. Context storage benchmarking + +This tutorial shows how to benchmark context storages. + +For more info see [API reference](%doclink(api,utils.db_benchmark.benchmark)). +""" + +# %pip install dff[benchmark,json,pickle,postgresql,mongodb,redis,mysql,sqlite,ydb] + +# %% +from pathlib import Path +from platform import system +import tempfile + +import dff.utils.db_benchmark as benchmark + +# %% [markdown] +""" +## Context storage setup +""" + +# %% +# this cell is only required for pickle, shelve and sqlite databases +tutorial_dir = Path(tempfile.mkdtemp()) +db_path = tutorial_dir / "dbs" +db_path.mkdir() +sqlite_file = db_path / "sqlite.db" +sqlite_file.touch(exist_ok=True) +sqlite_separator = "///" if system() == "Windows" else "////" + +# %% +storages = { + "JSON": f"json://{db_path}/json.json", + "Pickle": f"pickle://{db_path}/pickle.pkl", + "Shelve": f"shelve://{db_path}/shelve", + "PostgreSQL": "postgresql+asyncpg://postgres:pass@localhost:5432/test", + "MongoDB": "mongodb://admin:pass@localhost:27017/admin", + "Redis": "redis://:pass@localhost:6379/0", + "MySQL": "mysql+asyncmy://root:pass@localhost:3307/test", + "SQLite": f"sqlite+aiosqlite:{sqlite_separator}{sqlite_file.absolute()}", + "YDB": "grpc://localhost:2136/local", +} + +# %% [markdown] +""" +## Saving benchmark results to a file + +Benchmark results are saved to files. + +For that there exist two functions: +%mddoclink(api,utils.db_benchmark.benchmark,benchmark_all) +and +%mddoclink(api,utils.db_benchmark.benchmark,save_results_to_file). + +Note: context storages passed into these functions will be cleared. + +Once the benchmark results are saved to a file, you can view and analyze them using two methods: + +* [Using the Report Function](#Using-the-report-function): This function + can display specified information from a given file. + By default, it prints the name and average metrics for each benchmark case. + +* [Using the Streamlit App](#Using-Streamlit-app): A Streamlit app + is available for viewing and comparing benchmark results. + You can upload benchmark result files using the app's "Benchmark sets" tab, + inspect individual results in the "View" tab, and compare metrics in the "Compare" tab. + +Benchmark results are saved according to a specific schema, +which can be found in the benchmark schema documentation. +Each database being benchmarked will have its own result file. + +### Configuration + +The first one is a higher-level wrapper of the second one. +The first function accepts +%mddoclink(api,utils.db_benchmark.benchmark,BenchmarkCase) +which configure databases that are being benchmark and configurations of the benchmarks. +The second function accepts only a single URI for the database and several benchmark configurations. +So, the second function is simpler to use, while the first function allows for more configuration +(e.g. having different databases benchmarked in a single file). + +Both function use +%mddoclink(api,utils.db_benchmark.benchmark,BenchmarkConfig) +to configure benchmark behaviour. +`BenchmarkConfig` is only an interface for benchmark configurations. +Its most basic implementation is +%mddoclink(api,utils.db_benchmark.basic_config,BasicBenchmarkConfig). + +DFF provides configuration presets in the +%mddoclink(api,utils.db_benchmark.basic_config) module, +covering various contexts, messages, and edge cases. +You can use these presets by passing them to the benchmark functions or create +your own configuration. + +To learn more about using presets see [Configuration presets](#Configuration-presets) + +Benchmark configs have several parameters: + +Setting `context_num` to 50 means that we'll run fifty cycles of writing and reading context. +This way we'll be able to get a more accurate average read/write time as well as +check if read/write times are dependent on the number of contexts in the storage. + +You can also configure the `dialog_len`, `message_dimensions` and `misc_dimensions` parameters. +This allows you to set the contexts you want your database to be benchmarked with. + +### File structure + +The files are saved according to [the schema]( +../_misc/benchmark_schema.json +). +""" + +# %% +for db_name, db_uri in storages.items(): + benchmark.benchmark_all( + file=tutorial_dir / f"{db_name}.json", + name="Tutorial benchmark", + description="Benchmark for tutorial", + db_uri=db_uri, + benchmark_configs={ + "simple_config": benchmark.BasicBenchmarkConfig( + context_num=50, + from_dialog_len=1, + to_dialog_len=5, + message_dimensions=(3, 10), + misc_dimensions=(3, 10), + ), + }, + ) + +# %% [markdown] +""" +Running the cell above will create a file with benchmark results for every benchmarked DB: +""" + +# %% +list(tutorial_dir.iterdir()) + +# %% [markdown] +""" +## Viewing benchmark results + +Now that the results are saved to a file you can either view them using the +%mddoclink(api,utils.db_benchmark.report,report) +function or [our streamlit app]( +../_misc/benchmark_streamlit.py +). +""" + +# %% [markdown] +""" +### Using the report function + +The report function will print specified information from a given file. + +By default it prints the name and average metrics for each case. +""" + +# %% +benchmark.report( + file=tutorial_dir / "Shelve.json", display={"name", "config", "metrics"} +) + +# %% [markdown] +""" +### Using Streamlit app + +To run the app, execute: + +``` +# download file +curl https://deeppavlov.github.io/dialog_flow_framework/_misc/benchmark_streamlit.py \ +-o benchmark_streamlit.py +# install dependencies +pip install dff[benchmark] +# run +streamlit run benchmark_streamlit.py +``` + +You can upload files with benchmark results using the first tab of the app ("Benchmark sets"): + +.. figure:: ../_static/images/benchmark_sets.png + +The second tab ("View") lets you inspect individual benchmark results. +It also allows you to add a specific benchmark result +to the "Compare" tab via the button in the top-right corner. + +.. figure:: ../_static/images/benchmark_view.png + +In the "Compare" tab you can view main metrics (write, read, update, read+update) +of previously added benchmark results: + +.. figure:: ../_static/images/benchmark_compare.png + +"Mass compare" tab saves you the trouble of manually adding +to compare every benchmark result from a single file. + +.. figure:: ../_static/images/benchmark_mass_compare.png +""" + +# %% [markdown] +""" +## Additional information + +### Configuration presets + +The +%mddoclink(api,utils.db_benchmark.basic_config) +module also includes a dictionary containing configuration presets. +Those cover various contexts and messages as well as some edge cases. +""" + +# %% +print(benchmark.basic_config.basic_configurations.keys()) + +# %% [markdown] +""" +To use configuration presets, simply pass them to benchmark functions: + + benchmark.benchmark_all( + ..., + benchmark_configs=benchmark.basic_configurations + ) +""" + +# %% [markdown] +""" +### Custom configuration + +If the basic configuration is not enough for you, you can create your own. + +To do so, inherit from the +%mddoclink(api,utils.db_benchmark.benchmark,BenchmarkConfig) +class. You need to define three methods: + +- `get_context` -- method to get initial contexts. +- `info` -- method for getting display info representing the configuration. +- `context_updater` -- method for updating contexts. +""" diff --git a/_sources/tutorials/tutorials.messengers.telegram.1_basic.py.txt b/_sources/tutorials/tutorials.messengers.telegram.1_basic.py.txt new file mode 100644 index 0000000000..45fccc2729 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.1_basic.py.txt @@ -0,0 +1,92 @@ +# %% [markdown] +""" +# Telegram: 1. Basic + +The following tutorial shows how to run a regular DFF script in Telegram. +It asks users for the '/start' command and then loops in one place. + +Here, %mddoclink(api,messengers.telegram.interface,PollingTelegramInterface) +class and [telebot](https://pytba.readthedocs.io/en/latest/index.html) +library are used for accessing telegram API in polling mode. + +Telegram API token is required to access telegram API. +""" + +# %pip install dff[telegram] + +# %% +import os + +from dff.script import conditions as cnd +from dff.script import labels as lbl +from dff.script import RESPONSE, TRANSITIONS, Message +from dff.messengers.telegram import PollingTelegramInterface +from dff.pipeline import Pipeline +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +In order to integrate your script with Telegram, you need an instance of +`TelegramMessenger` class and one of the following interfaces: +`PollingMessengerInterface` or `WebhookMessengerInterface`. + +`TelegramMessenger` encapsulates the bot logic. +Like Telebot, `TelegramMessenger` only requires a token to run. +However, all parameters from the Telebot class can be passed as keyword arguments. + +The two interfaces connect the bot to Telegram. They can be passed directly +to the DFF `Pipeline` instance. +""" + + +# %% +script = { + "greeting_flow": { + "start_node": { + TRANSITIONS: { + "greeting_node": cnd.exact_match(Message(text="/start")) + }, + }, + "greeting_node": { + RESPONSE: Message(text="Hi"), + TRANSITIONS: {lbl.repeat(): cnd.true()}, + }, + "fallback_node": { + RESPONSE: Message(text="Please, repeat the request"), + TRANSITIONS: { + "greeting_node": cnd.exact_match(Message(text="/start")) + }, + }, + } +} + +# this variable is only for testing +happy_path = ( + (Message(text="/start"), Message(text="Hi")), + (Message(text="Hi"), Message(text="Hi")), + (Message(text="Bye"), Message(text="Hi")), +) + + +# %% +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), + messenger_interface=interface, + # The interface can be passed as a pipeline argument. +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.2_buttons.py.txt b/_sources/tutorials/tutorials.messengers.telegram.2_buttons.py.txt new file mode 100644 index 0000000000..efbbf2ab47 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.2_buttons.py.txt @@ -0,0 +1,192 @@ +# %% [markdown] +""" +# Telegram: 2. Buttons + +This tutorial shows how to display and hide a basic keyboard in Telegram. + +Here, %mddoclink(api,messengers.telegram.message,TelegramMessage) +class is used to represent telegram message, +%mddoclink(api,messengers.telegram.message,TelegramUI) and +%mddoclink(api,messengers.telegram.message,RemoveKeyboard) +classes are used for configuring additional telegram message features. + +Different %mddoclink(api,script.core.message,message) +classes are used for representing different common message features, +like Attachment, Audio, Button, Image, etc. +""" + + +# %pip install dff[telegram] + +# %% +import os + +import dff.script.conditions as cnd +from dff.script import TRANSITIONS, RESPONSE +from dff.script.core.message import Button +from dff.pipeline import Pipeline +from dff.messengers.telegram import ( + PollingTelegramInterface, + TelegramUI, + TelegramMessage, + RemoveKeyboard, +) +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +To display or hide a keyboard, you can utilize the `ui` field of the `TelegramMessage` class. +It can be initialized either with a `TelegramUI` instance or with a custom telebot keyboard. + +Passing an instance of `RemoveKeyboard` to the `ui` field will indicate that the keyboard +should be removed. +""" + + +# %% +script = { + "root": { + "start": { + TRANSITIONS: { + ("general", "native_keyboard"): ( + lambda ctx, _: ctx.last_request.text + in ("/start", "/restart") + ), + }, + }, + "fallback": { + RESPONSE: TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + TRANSITIONS: { + ("general", "native_keyboard"): ( + lambda ctx, _: ctx.last_request.text + in ("/start", "/restart") + ), + }, + }, + }, + "general": { + "native_keyboard": { + RESPONSE: TelegramMessage( + text="Question: What's 2 + 2?", + # In this case, we use telegram-specific classes. + # They derive from the generic ones and include more options, + # e.g. simple keyboard or inline keyboard. + ui=TelegramUI( + buttons=[ + Button(text="5"), + Button(text="4"), + ], + is_inline=False, + row_width=4, + ), + ), + TRANSITIONS: { + ("general", "success"): cnd.exact_match( + TelegramMessage(text="4") + ), + ("general", "fail"): cnd.true(), + }, + }, + "success": { + RESPONSE: TelegramMessage( + **{"text": "Success!", "ui": RemoveKeyboard()} + ), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + "fail": { + RESPONSE: TelegramMessage( + **{ + "text": "Incorrect answer, type anything to try again", + "ui": RemoveKeyboard(), + } + ), + TRANSITIONS: {("general", "native_keyboard"): cnd.true()}, + }, + }, +} + +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + +# this variable is only for testing +happy_path = ( + ( + TelegramMessage(text="/start"), + TelegramMessage( + text="Question: What's 2 + 2?", + ui=TelegramUI( + buttons=[ + Button(text="5"), + Button(text="4"), + ], + is_inline=False, + row_width=4, + ), + ), + ), + ( + TelegramMessage(text="5"), + TelegramMessage( + text="Incorrect answer, type anything to try again", + ui=RemoveKeyboard(), + ), + ), + ( + TelegramMessage(text="ok"), + TelegramMessage( + text="Question: What's 2 + 2?", + ui=TelegramUI( + buttons=[ + Button(text="5"), + Button(text="4"), + ], + is_inline=False, + row_width=4, + ), + ), + ), + ( + TelegramMessage(text="4"), + TelegramMessage(text="Success!", ui=RemoveKeyboard()), + ), + ( + TelegramMessage(text="Yay!"), + TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + ), + ( + TelegramMessage(text="/start"), + TelegramMessage( + text="Question: What's 2 + 2?", + ui=TelegramUI( + buttons=[ + Button(text="5"), + Button(text="4"), + ], + is_inline=False, + row_width=4, + ), + ), + ), +) + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + messenger_interface=interface, +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.py.txt b/_sources/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.py.txt new file mode 100644 index 0000000000..10d54927ff --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.3_buttons_with_callback.py.txt @@ -0,0 +1,181 @@ +# %% [markdown] +""" +# Telegram: 3. Buttons with Callback + +This tutorial demonstrates, how to add an inline keyboard and utilize +inline queries. + +Here, %mddoclink(api,messengers.telegram.message,TelegramMessage) +class is used to represent telegram message, +%mddoclink(api,messengers.telegram.message,TelegramUI) and +%mddoclink(api,messengers.telegram.message,RemoveKeyboard) +classes are used for configuring additional telegram message features. + +Different %mddoclink(api,script.core.message,message) +classes are used for representing different common message features, +like Attachment, Audio, Button, Image, etc. +""" + + +# %pip install dff[telegram] + +# %% +import os + +import dff.script.conditions as cnd +from dff.script import TRANSITIONS, RESPONSE +from dff.pipeline import Pipeline +from dff.script.core.message import Button +from dff.messengers.telegram import ( + PollingTelegramInterface, + TelegramUI, + TelegramMessage, +) +from dff.messengers.telegram.message import _ClickButton +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +If you want to send an inline keyboard to your Telegram chat, +set `is_inline` field of the `TelegramUI` instance to `True` +(note that it is inline by default, so you could also omit it). + +Pushing a button of an inline keyboard results in a callback +query being sent to your bot. The data of the query +is stored in the `callback_query` field of a user `TelegramMessage`. +""" + + +# %% +script = { + "root": { + "start": { + TRANSITIONS: { + ("general", "keyboard"): ( + lambda ctx, _: ctx.last_request.text + in ("/start", "/restart") + ), + }, + }, + "fallback": { + RESPONSE: TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + TRANSITIONS: { + ("general", "keyboard"): ( + lambda ctx, _: ctx.last_request.text + in ("/start", "/restart") + ) + }, + }, + }, + "general": { + "keyboard": { + RESPONSE: TelegramMessage( + **{ + "text": "Starting test! What's 9 + 10?", + "ui": TelegramUI( + buttons=[ + Button(text="19", payload="correct"), + Button(text="21", payload="wrong"), + ], + is_inline=True, + ), + } + ), + TRANSITIONS: { + ("general", "success"): cnd.exact_match( + TelegramMessage(callback_query="correct") + ), + ("general", "fail"): cnd.exact_match( + TelegramMessage(callback_query="wrong") + ), + }, + }, + "success": { + RESPONSE: TelegramMessage(text="Success!"), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + "fail": { + RESPONSE: TelegramMessage( + text="Incorrect answer, type anything to try again" + ), + TRANSITIONS: {("general", "keyboard"): cnd.true()}, + }, + }, +} + +# this variable is only for testing +happy_path = ( + ( + TelegramMessage(text="/start"), + TelegramMessage( + text="Starting test! What's 9 + 10?", + ui=TelegramUI( + buttons=[ + Button(text="19", payload="correct"), + Button(text="21", payload="wrong"), + ], + ), + ), + ), + ( + TelegramMessage(callback_query=_ClickButton(button_index=1)), + TelegramMessage(text="Incorrect answer, type anything to try again"), + ), + ( + TelegramMessage(text="try again"), + TelegramMessage( + text="Starting test! What's 9 + 10?", + ui=TelegramUI( + buttons=[ + Button(text="19", payload="correct"), + Button(text="21", payload="wrong"), + ], + ), + ), + ), + ( + TelegramMessage(callback_query=_ClickButton(button_index=0)), + TelegramMessage(text="Success!"), + ), + ( + TelegramMessage(text="Yay!"), + TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + ), + ( + TelegramMessage(text="/restart"), + TelegramMessage( + text="Starting test! What's 9 + 10?", + ui=TelegramUI( + buttons=[ + Button(text="19", payload="correct"), + Button(text="21", payload="wrong"), + ], + ), + ), + ), +) + +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + messenger_interface=interface, +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.4_conditions.py.txt b/_sources/tutorials/tutorials.messengers.telegram.4_conditions.py.txt new file mode 100644 index 0000000000..d6ba4f4c52 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.4_conditions.py.txt @@ -0,0 +1,143 @@ +# %% [markdown] +""" +# Telegram: 4. Conditions + +This tutorial shows how to process Telegram updates in your script +and reuse handler triggers from the `pytelegrambotapi` library. + +Here, %mddoclink(api,messengers.telegram.messenger,telegram_condition) +function is used for graph navigation according to Telegram events. +""" + + +# %pip install dff[telegram] + +# %% +import os + +from dff.script import TRANSITIONS, RESPONSE + +from dff.messengers.telegram import ( + PollingTelegramInterface, + telegram_condition, + UpdateType, +) +from dff.pipeline import Pipeline +from dff.messengers.telegram import TelegramMessage +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +In our Telegram module, we adopted the system of filters +available in the `pytelegrambotapi` library. + +You can use `telegram_condition` to filter text messages from telegram in various ways. + +- Setting the `update_type` will allow filtering by update type: + if you want the condition to trigger only on updates of the type `edited_message`, + set it to `UpdateType.EDITED_MESSAGE`. + The field defaults to `message`. +- Setting the `command` argument will cause the telegram_condition to only react to listed commands. +- `func` argument on the other hand allows you to define arbitrary conditions. +- `regexp` creates a regular expression filter, etc. + +Note: +It is possible to use `cnd.exact_match` as a condition (as seen in previous tutorials). +However, the functionality of that approach is lacking: +At this moment only two fields of `Message` are set during update processing: + +- `text` stores the `text` field of `message` updates +- `callback_query` stores the `data` field of `callback_query` updates + +For more information see tutorial `3_buttons_with_callback.py`. +""" + + +# %% +script = { + "greeting_flow": { + "start_node": { + TRANSITIONS: { + "node1": telegram_condition(commands=["start", "restart"]) + }, + }, + "node1": { + RESPONSE: TelegramMessage(text="Hi, how are you?"), + TRANSITIONS: { + "node2": telegram_condition( + update_type=UpdateType.MESSAGE, regexp="fine" + ) + }, + # this is the same as + # TRANSITIONS: {"node2": telegram_condition(regexp="fine")}, + }, + "node2": { + RESPONSE: TelegramMessage( + text="Good. What do you want to talk about?" + ), + TRANSITIONS: { + "node3": telegram_condition( + func=lambda msg: "music" in msg.text + ) + }, + }, + "node3": { + RESPONSE: TelegramMessage( + text="Sorry, I can not talk about music now." + ), + TRANSITIONS: { + "node4": telegram_condition(update_type=UpdateType.ALL) + }, + # This condition is true for any type of update + }, + "node4": { + RESPONSE: TelegramMessage(text="bye"), + TRANSITIONS: {"node1": telegram_condition()}, + # This condition is true if the last update is of type `message` + }, + "fallback_node": { + RESPONSE: TelegramMessage(text="Ooops"), + TRANSITIONS: { + "node1": telegram_condition(commands=["start", "restart"]) + }, + }, + } +} + +# this variable is only for testing +happy_path = ( + (TelegramMessage(text="/start"), TelegramMessage(text="Hi, how are you?")), + ( + TelegramMessage(text="I'm fine"), + TelegramMessage(text="Good. What do you want to talk about?"), + ), + ( + TelegramMessage(text="About music"), + TelegramMessage(text="Sorry, I can not talk about music now."), + ), + (TelegramMessage(text="ok"), TelegramMessage(text="bye")), + (TelegramMessage(text="bye"), TelegramMessage(text="Hi, how are you?")), +) + + +# %% +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), + messenger_interface=interface, +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.5_conditions_with_media.py.txt b/_sources/tutorials/tutorials.messengers.telegram.5_conditions_with_media.py.txt new file mode 100644 index 0000000000..7f80ae3c68 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.5_conditions_with_media.py.txt @@ -0,0 +1,204 @@ +# %% [markdown] +""" +# Telegram: 5. Conditions with Media + +This tutorial shows how to use media-related logic in your script. + +Here, %mddoclink(api,messengers.telegram.messenger,telegram_condition) +function is used for graph navigation according to Telegram events. + +Different %mddoclink(api,script.core.message,message) +classes are used for representing different common message features, +like Attachment, Audio, Button, Image, etc. +""" + + +# %pip install dff[telegram] + +# %% +import os + +from telebot.types import Message + +import dff.script.conditions as cnd +from dff.script import Context, TRANSITIONS, RESPONSE +from dff.script.core.message import Image, Attachments +from dff.messengers.telegram import ( + PollingTelegramInterface, + TelegramMessage, + telegram_condition, +) +from dff.pipeline import Pipeline +from dff.utils.testing.common import is_interactive_mode + + +# %% + +picture_url = "https://avatars.githubusercontent.com/u/29918795?s=200&v=4" + + +# %% [markdown] +""" +To filter user messages depending on whether or not media files were sent, +you can use the `content_types` parameter of the `telegram_condition`. +""" + + +# %% +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +script = { + "root": { + "start": { + TRANSITIONS: { + ("pics", "ask_picture"): telegram_condition( + commands=["start", "restart"] + ) + }, + }, + "fallback": { + RESPONSE: TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + TRANSITIONS: { + ("pics", "ask_picture"): telegram_condition( + commands=["start", "restart"] + ) + }, + }, + }, + "pics": { + "ask_picture": { + RESPONSE: TelegramMessage(text="Send me a picture"), + TRANSITIONS: { + ("pics", "send_one"): cnd.any( + [ + # Telegram can put photos + # both in 'photo' and 'document' fields. + # We should consider both cases + # when we check the message for media. + telegram_condition(content_types=["photo"]), + telegram_condition( + func=lambda message: ( + # check attachments in message properties + message.document + and message.document.mime_type == "image/jpeg" + ), + content_types=["document"], + ), + ] + ), + ("pics", "send_many"): telegram_condition( + content_types=["text"] + ), + ("pics", "ask_picture"): cnd.true(), + }, + }, + "send_one": { + # An HTTP path or a path to a local file can be used here. + RESPONSE: TelegramMessage( + text="Here's my picture!", + attachments=Attachments(files=[Image(source=picture_url)]), + ), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + "send_many": { + RESPONSE: TelegramMessage( + text="Look at my pictures!", + # An HTTP path or a path to a local file can be used here. + attachments=Attachments(files=[Image(source=picture_url)] * 2), + ), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + }, +} + + +# testing +happy_path = ( + ( + TelegramMessage(text="/start"), + TelegramMessage(text="Send me a picture"), + ), + ( + TelegramMessage( + attachments=Attachments(files=[Image(source=picture_url)]) + ), + TelegramMessage( + text="Here's my picture!", + attachments=Attachments(files=[Image(source=picture_url)]), + ), + ), + ( + TelegramMessage(text="ok"), + TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + ), + ( + TelegramMessage(text="/restart"), + TelegramMessage(text="Send me a picture"), + ), + ( + TelegramMessage(text="No"), + TelegramMessage( + text="Look at my pictures!", + attachments=Attachments(files=[Image(source=picture_url)] * 2), + ), + ), + ( + TelegramMessage(text="ok"), + TelegramMessage( + text="Finishing test, send /restart command to restart" + ), + ), + ( + TelegramMessage(text="/restart"), + TelegramMessage(text="Send me a picture"), + ), +) + + +# %% +def extract_data(ctx: Context, _: Pipeline): # A function to extract data with + message = ctx.last_request + if message is None: + return ctx + update = getattr(message, "update", None) + if update is None: + return ctx + if not isinstance(update, Message): + return ctx + if ( + # check attachments in update properties + not update.photo + and not (update.document and update.document.mime_type == "image/jpeg") + ): + return ctx + photo = update.document or update.photo[-1] + file = interface.messenger.get_file(photo.file_id) + result = interface.messenger.download_file(file.file_path) + with open("photo.jpg", "wb+") as new_file: + new_file.write(result) + return ctx + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + messenger_interface=interface, + pre_services=[extract_data], +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.6_conditions_extras.py.txt b/_sources/tutorials/tutorials.messengers.telegram.6_conditions_extras.py.txt new file mode 100644 index 0000000000..db161df036 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.6_conditions_extras.py.txt @@ -0,0 +1,124 @@ +# %% [markdown] +""" +# Telegram: 6. Conditions Extras + +This tutorial shows how to use additional update filters +inherited from the `pytelegrambotapi` library. + +%mddoclink(api,messengers.telegram.messenger,telegram_condition) +function and different types of %mddoclink(api,messengers.telegram.messenger,UpdateType) +are used for telegram message type checking. +""" + + +# %pip install dff[telegram] + +# %% +import os + +from dff.script import TRANSITIONS, RESPONSE, GLOBAL +import dff.script.conditions as cnd +from dff.messengers.telegram import ( + PollingTelegramInterface, + TelegramMessage, + telegram_condition, + UpdateType, +) +from dff.pipeline import Pipeline +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +In our Telegram module, we adopted the system of filters +available in the `pytelegrambotapi` library. + +Aside from `MESSAGE` you can use +other triggers to interact with the api. In this tutorial, we use +handlers of other type as global conditions that trigger a response +from the bot. + +Here, we use the following triggers: + +* `chat_join_request`: join request is sent to the chat where the bot is. +* `my_chat_member`: triggered when the bot is invited to a chat. +* `inline_query`: triggered when an inline query is being sent to the bot. + +The other available conditions are: + +* `channel_post`: new post is created in a channel the bot is subscribed to; +* `edited_channel_post`: post is edited in a channel the bot is subscribed to; +* `shipping_query`: shipping query is sent by the user; +* `pre_checkout_query`: order confirmation is sent by the user; +* `poll`: poll is sent to the chat; +* `poll_answer`: users answered the poll sent by the bot. + +You can read more on those in the Telegram documentation or in the docs for the `telebot` library. +""" + + +# %% +script = { + GLOBAL: { + TRANSITIONS: { + ("greeting_flow", "node1"): cnd.any( + [ + # say hi when invited to a chat + telegram_condition( + update_type=UpdateType.CHAT_JOIN_REQUEST, + func=lambda x: True, + ), + # say hi when someone enters the chat + telegram_condition( + update_type=UpdateType.MY_CHAT_MEMBER, + func=lambda x: True, + ), + ] + ), + # send a message when inline query is received + ("greeting_flow", "node2"): telegram_condition( + update_type=UpdateType.INLINE_QUERY, + ), + }, + }, + "greeting_flow": { + "start_node": { + TRANSITIONS: { + "node1": telegram_condition(commands=["start", "restart"]) + }, + }, + "node1": { + RESPONSE: TelegramMessage(text="Hi"), + TRANSITIONS: {"start_node": cnd.true()}, + }, + "node2": { + RESPONSE: TelegramMessage(text="Inline query received."), + TRANSITIONS: {"start_node": cnd.true()}, + }, + "fallback_node": { + RESPONSE: TelegramMessage(text="Ooops"), + }, + }, +} + + +# %% +interface = PollingTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +pipeline = Pipeline.from_script( + script=script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), + messenger_interface=interface, +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.7_polling_setup.py.txt b/_sources/tutorials/tutorials.messengers.telegram.7_polling_setup.py.txt new file mode 100644 index 0000000000..d070e47280 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.7_polling_setup.py.txt @@ -0,0 +1,64 @@ +# %% [markdown] +""" +# Telegram: 7. Polling Setup + +The following tutorial shows how to configure `PollingTelegramInterface`. + +See %mddoclink(api,messengers.telegram.interface,PollingTelegramInterface) +for more information. +""" + +# %pip install dff[telegram] + +# %% +import os + +from dff.messengers.telegram.interface import PollingTelegramInterface +from dff.pipeline import Pipeline + +from dff.utils.testing.common import is_interactive_mode +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from telebot.util import update_types + + +# %% [markdown] +""" +`PollingTelegramInterface` can be configured with the same parameters +that are used in the `pytelegrambotapi` library, specifically: + +* interval - time between calls to the API. +* allowed updates - updates that should be fetched. +* timeout - general timeout. +* long polling timeout - timeout for polling. +""" + + +# %% +interface = PollingTelegramInterface( + token=os.environ["TG_BOT_TOKEN"], + interval=2, + allowed_updates=update_types, + timeout=30, + long_polling_timeout=30, +) + + +# testing +happy_path = HAPPY_PATH + + +# %% +pipeline = Pipeline.from_script( + *TOY_SCRIPT_ARGS, + messenger_interface=interface, + # The interface can be passed as a pipeline argument +) + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.telegram.8_webhook_setup.py.txt b/_sources/tutorials/tutorials.messengers.telegram.8_webhook_setup.py.txt new file mode 100644 index 0000000000..a7f4fd68f5 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.telegram.8_webhook_setup.py.txt @@ -0,0 +1,61 @@ +# %% [markdown] +""" +# Telegram: 8. Webhook Setup + +The following tutorial shows how to use `CallbackTelegramInterface` +that makes your bot accessible through a public webhook. + +See %mddoclink(api,messengers.common.interface,CallbackMessengerInterface) +for more information. +""" + +# %pip install dff[telegram] flask + +# %% +import os + +from dff.messengers.telegram import ( + CallbackTelegramInterface, +) +from dff.pipeline import Pipeline +from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH +from dff.utils.testing.common import is_interactive_mode + + +# %% [markdown] +""" +To set up a webhook, you need a messenger and a web application instance. +This class can be configured with the following parameters: + +* app - Flask application. You can pass an application with an arbitrary + number of pre-configured routes. Created automatically if not set. +* host - application host. +* port - application port. +* endpoint - bot access endpoint. +* full_uri - full public address of the endpoint. HTTPS should be enabled + for successful configuration. +""" + + +# %% +interface = CallbackTelegramInterface(token=os.environ["TG_BOT_TOKEN"]) + + +# %% +pipeline = Pipeline.from_script( + *TOY_SCRIPT_ARGS, + messenger_interface=interface, + # The interface can be passed as a pipeline argument +) + +# testing +happy_path = HAPPY_PATH + + +def main(): + pipeline.run() + + +if __name__ == "__main__" and is_interactive_mode(): + # prevent run during doc building + main() diff --git a/_sources/tutorials/tutorials.messengers.web_api_interface.1_fastapi.py.txt b/_sources/tutorials/tutorials.messengers.web_api_interface.1_fastapi.py.txt new file mode 100644 index 0000000000..199d1eb282 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.web_api_interface.1_fastapi.py.txt @@ -0,0 +1,113 @@ +# %% [markdown] +""" +# Web API: 1. FastAPI + +This tutorial shows how to create an API for DFF using FastAPI and +introduces messenger interfaces. + +You can see the result at http://127.0.0.1:8000/docs. + +Here, %mddoclink(api,messengers.common.interface,CallbackMessengerInterface) +is used to process requests. + +%mddoclink(api,script.core.message,Message) is used in creating a JSON Schema for the endpoint. +""" + +# %pip install dff uvicorn fastapi + +# %% +from dff.messengers.common.interface import CallbackMessengerInterface +from dff.script import Message +from dff.pipeline import Pipeline +from dff.utils.testing import TOY_SCRIPT_ARGS, is_interactive_mode + +import uvicorn +from pydantic import BaseModel +from fastapi import FastAPI + +# %% [markdown] +""" +Messenger interfaces establish communication between users and the pipeline. +They manage message channel initialization and termination +as well as pipeline execution on every user request. +There are two built-in messenger interface types that can be extended +through inheritance: + +* `PollingMessengerInterface` - Starts polling for user requests + in a loop upon initialization, + it has following methods: + + * `_request()` - Method that is used to retrieve user requests from a messenger, + should return list of tuples: (user request, unique dialog id). + * `_respond(responses)` - Method that sends responses generated by pipeline + to users through a messenger, + accepts list of dialog `Contexts`. + * `_on_exception(e)` - Method that is called when a critical exception occurs + i.e. exception from context storage or messenger interface, not a service exception. + + Such exceptions lead to the termination of the loop. + * `connect(pipeline_runner, loop, timeout)` - + Method that starts the polling loop. + + This method is called inside `pipeline.run` method. + + It accepts 3 arguments: + + * a callback that runs pipeline, + * a function that should return True to continue polling, + * and time to wait between loop executions. + +* `CallbackMessengerInterface` - Creates message channel + and provides a callback for pipeline execution, + it has following methods: + + * `on_request(request, ctx_id)` or `on_request_async(request, ctx_id)` - + Method that should be called each time + user provides new input to pipeline, + returns dialog Context. + * `connect(pipeline_runner)` - Method that sets `pipeline_runner` as + a function to be called inside `on_request`. + + This method is called inside `pipeline.run` method. + +You can find API reference for these classes [here](%doclink(api,messengers.common.interface)). + +Here the default `CallbackMessengerInterface` is used to setup +communication between the pipeline on the server side and the messenger client. +""" + +# %% +messenger_interface = CallbackMessengerInterface() +# CallbackMessengerInterface instantiating the dedicated messenger interface +pipeline = Pipeline.from_script( + *TOY_SCRIPT_ARGS, messenger_interface=messenger_interface +) + + +# %% +app = FastAPI() + + +class Output(BaseModel): + user_id: str + response: Message + + +@app.post("/chat", response_model=Output) +async def respond( + user_id: str, + user_message: Message, +): + context = await messenger_interface.on_request_async(user_message, user_id) + return {"user_id": user_id, "response": context.last_response} + + +# %% +if __name__ == "__main__": + if is_interactive_mode(): # do not run this during doc building + pipeline.run() # runs the messenger_interface.connect method + uvicorn.run( + app, + host="127.0.0.1", + port=8000, + ) diff --git a/_sources/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.py.txt b/_sources/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.py.txt new file mode 100644 index 0000000000..4719d14f36 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.web_api_interface.2_websocket_chat.py.txt @@ -0,0 +1,116 @@ +# %% [markdown] +""" +# Web API: 2. WebSocket Chat + +This tutorial shows how to create a Web chat on FastAPI using websockets. + +You can see the result at http://127.0.0.1:8000/. + +This tutorial is a modified version of the FastAPI tutorial on WebSockets: +https://fastapi.tiangolo.com/advanced/websockets/. + +As mentioned in that tutorial, + +> ... for this example, we'll use a very simple HTML document with some JavaScript, +> all inside a long string. +> This, of course, is not optimal and you wouldn't use it for production. + +Here, %mddoclink(api,messengers.common.interface,CallbackMessengerInterface) +is used to process requests. + +%mddoclink(api,script.core.message,Message) is used to represent text messages. +""" + +# %pip install dff uvicorn fastapi + +# %% +from dff.messengers.common.interface import CallbackMessengerInterface +from dff.script import Message +from dff.pipeline import Pipeline +from dff.utils.testing import TOY_SCRIPT_ARGS, is_interactive_mode + +import uvicorn +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + + +# %% +messenger_interface = CallbackMessengerInterface() +pipeline = Pipeline.from_script( + *TOY_SCRIPT_ARGS, messenger_interface=messenger_interface +) + + +# %% +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + +
+
    +
+ + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await websocket.accept() + try: + while True: + data = await websocket.receive_text() + await websocket.send_text(f"User: {data}") + request = Message(text=data) + context = await messenger_interface.on_request_async( + request, client_id + ) + response = context.last_response.text + if response is not None: + await websocket.send_text(f"Bot: {response}") + else: + await websocket.send_text("Bot did not return text.") + except WebSocketDisconnect: # ignore disconnections + pass + + +# %% +if __name__ == "__main__": + if is_interactive_mode(): # do not run this during doc building + pipeline.run() + uvicorn.run( + app, + host="127.0.0.1", + port=8000, + ) diff --git a/_sources/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.py.txt b/_sources/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.py.txt new file mode 100644 index 0000000000..ccd1e13a77 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.py.txt @@ -0,0 +1,150 @@ +# %% [markdown] +""" +# Web API: 3. Load testing with Locust + +This tutorial shows how to use an API endpoint created in the FastAPI tutorial in load testing. +""" + +# %pip install dff locust + +# %% [markdown] +""" +## Running Locust + +1. Run this file directly: + ```bash + python {file_name} + ``` +2. Run locust targeting this file: + ```bash + locust -f {file_name} + ``` +3. Run from python: + ```python + import sys + from locust import main + + sys.argv = ["locust", "-f", {file_name}] + main.main() + ``` + +You should see the result at http://127.0.0.1:8089. + +Make sure that your POST endpoint is also running (run the FastAPI tutorial). +""" + + +# %% +################################################################################ +# this patch is only needed to run this file in IPython kernel +# and can be safely removed +import gevent.monkey + +gevent.monkey.patch_all() +################################################################################ + + +# %% +import uuid +import time +import sys + +from locust import FastHttpUser, task, constant, main + +from dff.script import Message +from dff.utils.testing import HAPPY_PATH, is_interactive_mode + + +# %% +class DFFUser(FastHttpUser): + wait_time = constant(1) + + def check_happy_path(self, happy_path): + """ + Check a happy path. + For each `(request, response)` pair in `happy_path`: + 1. Send request to the API endpoint and catch its response. + 2. Compare API response with the `response`. + If they do not match, fail the request. + + :param happy_path: + An iterable of tuples of + `(Message, Message | Callable(Message->str|None) | None)`. + + If the second element is `Message`, + check that API response matches it. + + If the second element is `None`, + do not check the API response. + + If the second element is a `Callable`, + call it with the API response as its argument. + If the function returns a string, + that string is considered an error message. + If the function returns `None`, + the API response is considered correct. + """ + user_id = str(uuid.uuid4()) + + for request, response in happy_path: + with self.client.post( + f"/chat?user_id={user_id}", + headers={ + "accept": "application/json", + "Content-Type": "application/json", + }, + # Name is the displayed name of the request. + name=f"/chat?user_message={request.json()}", + data=request.json(), + catch_response=True, + ) as candidate_response: + text_response = Message.model_validate( + candidate_response.json().get("response") + ) + + if response is not None: + if callable(response): + error_message = response(text_response) + if error_message is not None: + candidate_response.failure(error_message) + elif text_response != response: + candidate_response.failure( + f"Expected: {response.model_dump_json()}\n" + f"Got: {text_response.model_dump_json()}" + ) + + time.sleep(self.wait_time()) + + @task(3) # <- this task is 3 times more likely than the other + def dialog_1(self): + self.check_happy_path(HAPPY_PATH) + + @task + def dialog_2(self): + def check_first_message(msg: Message) -> str | None: + if msg.text is None: + return f"Message does not contain text: {msg.model_dump_json()}" + if "Hi" not in msg.text: + return ( + f'"Hi" is not in the response message: ' + f"{msg.model_dump_json()}" + ) + return None + + self.check_happy_path( + [ + # a function can be used to check the return message + (Message(text="Hi"), check_first_message), + # a None is used if return message should not be checked + (Message(text="i'm fine, how are you?"), None), + # this should fail + (Message(text="Hi"), check_first_message), + ] + ) + + +# %% +if __name__ == "__main__": + if is_interactive_mode(): + sys.argv = ["locust", "-f", __file__] + main.main() diff --git a/_sources/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.py.txt b/_sources/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.py.txt new file mode 100644 index 0000000000..90dd195dd5 --- /dev/null +++ b/_sources/tutorials/tutorials.messengers.web_api_interface.4_streamlit_chat.py.txt @@ -0,0 +1,188 @@ +# %% [markdown] +# # Web API: 4. Streamlit chat interface +# +# This tutorial shows how to use an API endpoint created in the FastAPI tutorial +# in a Streamlit chat. +# +# A demonstration of the chat: +# ![demo](https://user-images.githubusercontent.com/61429541/238721597-ef88261d-e9e6-497d-ba68-0bcc9a765808.png) + +# %pip install dff streamlit streamlit-chat + +# %% [markdown] +# ## Running Streamlit: +# +# ```bash +# streamlit run {file_name} +# ``` + + +# %% [markdown] +# ## Module and package import + + +# %% +########################################################### +# This patch is only needed to import Message from dff. +# Streamlit Chat interface can be written without using it. +import asyncio + +loop = asyncio.new_event_loop() +asyncio.set_event_loop(loop) +########################################################### + + +# %% +import uuid +import itertools + +import requests +import streamlit as st +from streamlit_chat import message +import streamlit.components.v1 as components +from dff.script import Message + + +# %% [markdown] +# ## API configuration +# +# Here we define methods to contact the API endpoint. + + +# %% +API_URL = "http://localhost:8000/chat" + + +def query(payload, user_id) -> requests.Response: + response = requests.post( + API_URL + f"?user_id={user_id}", + headers={ + "accept": "application/json", + "Content-Type": "application/json", + }, + json=payload, + ) + return response + + +# %% [markdown] +# ## Streamlit configuration +# +# Here we configure Streamlit page and initialize some session variables: +# +# 1. `user_id` -- stores user_id to be used in pipeline. +# 2. `bot_responses` -- a list of bot responses. +# 3. `user_requests` -- a list of user requests. + + +# %% +st.set_page_config(page_title="Streamlit DFF Chat", page_icon=":robot:") + +st.header("Streamlit DFF Chat") + +if "user_id" not in st.session_state: + st.session_state["user_id"] = str(uuid.uuid4()) + +if "bot_responses" not in st.session_state: + st.session_state["bot_responses"] = [] + +if "user_requests" not in st.session_state: + st.session_state["user_requests"] = [] + + +# %% [markdown] +# ## UI setup +# +# Here we configure elements that will be used in Streamlit to interact with the API. +# +# First we define a text input field which a user is supposed to type his requests into. +# Then we define a button that sends a query to the API, logs requests and responses, +# and clears the text field. + + +# %% +def send_and_receive(): + """ + Send text inside the input field. Receive response from API endpoint. + + Add both the request and response to `user_requests` and `bot_responses`. + + We do not call this function inside the `text_input.on_change` because then + we'd call it whenever the text field loses focus + (e.g. when a browser tab is switched). + """ + user_request = st.session_state["input"] + + if user_request == "": + return + + st.session_state["user_requests"].append(user_request) + + bot_response = query( + Message(text=user_request).model_dump(), + user_id=st.session_state["user_id"], + ) + bot_response.raise_for_status() + + bot_message = Message.model_validate(bot_response.json()["response"]).text + + # # Implementation without using Message: + # bot_response = query( + # {"text": user_request}, + # user_id=st.session_state["user_id"] + # ) + # bot_response.raise_for_status() + # + # bot_message = bot_response.json()["response"]["text"] + + st.session_state["bot_responses"].append(bot_message) + + st.session_state["input"] = "" + + +# %% +st.text_input("You: ", key="input") +st.button("Send", on_click=send_and_receive) + + +# %% [markdown] +# ### Component patch +# +# Here we add a component that presses the `Send` button whenever user presses the `Enter` key. + + +# %% +components.html( + """ + +""", + height=0, + width=0, +) + + +# %% [markdown] +# ### Message display +# +# Here we use the `streamlit-chat` package to display user requests and bot responses. + + +# %% +for i, bot_response, user_request in zip( + itertools.count(0), + st.session_state.get("bot_responses", []), + st.session_state.get("user_requests", []), +): + message(user_request, key=f"{i}_user", is_user=True) + message(bot_response, key=f"{i}_bot") diff --git a/_sources/tutorials/tutorials.pipeline.1_basics.py.txt b/_sources/tutorials/tutorials.pipeline.1_basics.py.txt new file mode 100644 index 0000000000..91d0dbbcd8 --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.1_basics.py.txt @@ -0,0 +1,84 @@ +# %% [markdown] +""" +# 1. Basics + +The following tutorial shows basic usage of `pipeline` +module as an extension to `dff.script.core`. + +Here, `__call__` (same as %mddoclink(api,pipeline.pipeline.pipeline,Pipeline.run)) +method is used to execute pipeline once. +""" + +# %pip install dff + +# %% +from dff.script import Context, Message + +from dff.pipeline import Pipeline + +from dff.utils.testing import ( + check_happy_path, + is_interactive_mode, + HAPPY_PATH, + TOY_SCRIPT, + TOY_SCRIPT_ARGS, +) + + +# %% [markdown] +""" +`Pipeline` is an object, that automates script execution and context management. +`from_script` method can be used to create +a pipeline of the most basic structure: +"preprocessors -> actor -> postprocessors" +as well as to define `context_storage` and `messenger_interface`. +Actor is a component of :py:class:`.Pipeline`, that contains the :py:class:`.Script` +and handles it. It is responsible for processing user input and determining +the appropriate response based on the current state of the conversation and the script. +These parameters usage will be shown in tutorials 2, 3 and 6. + +Here only required parameters are provided to pipeline. +`context_storage` will default to simple Python dict and +`messenger_interface` will never be used. +pre- and postprocessors lists are empty. +`Pipeline` object can be called with user input +as first argument and dialog id (any immutable object). +This call will return `Context`, +its `last_response` property will be actors response. +""" + +# %% +pipeline = Pipeline.from_script( + TOY_SCRIPT, + # Pipeline script object, defined in `dff.utils.testing.toy_script` + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + + +# %% [markdown] +""" +For the sake of brevity, other tutorials might use `TOY_SCRIPT_ARGS` to initialize pipeline: +""" + +# %% +assert TOY_SCRIPT_ARGS == ( + TOY_SCRIPT, + ("greeting_flow", "start_node"), + ("greeting_flow", "fallback_node"), +) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + # a function for automatic tutorial running (testing) with HAPPY_PATH + + # This runs tutorial in interactive mode if not in IPython env + # and if `DISABLE_INTERACTIVE_MODE` is not set + if is_interactive_mode(): + ctx_id = 0 # 0 will be current dialog (context) identification. + while True: + message = Message(text=input("Send request: ")) + ctx: Context = pipeline(message, ctx_id) + print(ctx.last_response) diff --git a/_sources/tutorials/tutorials.pipeline.2_pre_and_post_processors.py.txt b/_sources/tutorials/tutorials.pipeline.2_pre_and_post_processors.py.txt new file mode 100644 index 0000000000..f26fafa54a --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.2_pre_and_post_processors.py.txt @@ -0,0 +1,95 @@ +# %% [markdown] +""" +# 2. Pre- and postprocessors + +The following tutorial shows more advanced usage of `pipeline` +module as an extension to `dff.script.core`. + +Here, %mddoclink(api,script.core.context,Context.misc) +dictionary of context is used for storing additional data. +""" + +# %pip install dff + +# %% +import logging + +from dff.messengers.common import CLIMessengerInterface +from dff.script import Context, Message + +from dff.pipeline import Pipeline + +from dff.utils.testing import ( + check_happy_path, + is_interactive_mode, + HAPPY_PATH, + TOY_SCRIPT_ARGS, +) + +logger = logging.getLogger(__name__) + + +# %% [markdown] +""" +When Pipeline is created with `from_script` method, additional pre- +and postprocessors can be defined. +These can be any `ServiceBuilder` objects (defined in `types` module) +- callables, objects or dicts. +They are being turned into special `Service` objects (see tutorial 3), +that will be run before or after `Actor` respectively. +These services can be used to access external APIs, annotate user input, etc. + +Service callable signature can be one of the following: +`[ctx]`, `[ctx, pipeline]` or `[ctx, actor, info]` (see tutorial 3), +where: + +* `ctx` - Context of the current dialog. +* `pipeline` - The current pipeline. +* `info` - dictionary, containing information about + current service and pipeline execution state (see tutorial 4). + +Here a preprocessor ("ping") and a postprocessor ("pong") are added to pipeline. +They share data in `context.misc` - +a common place for sharing data between services and actor. +""" + + +# %% +def ping_processor(ctx: Context): + ctx.misc["ping"] = True + + +def pong_processor(ctx: Context): + ping = ctx.misc.get("ping", False) + ctx.misc["pong"] = ping + + +# %% +pipeline = Pipeline.from_script( + *TOY_SCRIPT_ARGS, + context_storage={}, # `context_storage` - a dictionary or + # a `DBContextStorage` instance, + # a place to store dialog contexts + messenger_interface=CLIMessengerInterface(), + # `messenger_interface` - a message channel adapter, + # it's not used in this tutorial + pre_services=[ping_processor], + post_services=[pong_processor], +) + + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + ctx_id = 0 # 0 will be current dialog (context) identification. + while True: + message = Message(text=input("Send request: ")) + ctx: Context = pipeline(message, ctx_id) + print(f"Response: {ctx.last_response}") + ping_pong = ctx.misc.get("ping", False) and ctx.misc.get( + "pong", False + ) + print( + f"Ping-pong exchange: {'completed' if ping_pong else 'failed'}." + ) + logger.info(f"Context misc: {ctx.misc}") diff --git a/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.py.txt b/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.py.txt new file mode 100644 index 0000000000..dbe7fb1e5b --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_basic.py.txt @@ -0,0 +1,97 @@ +# %% [markdown] +""" +# 3. Pipeline dict with services (basic) + +The following tutorial shows `pipeline` creation from +dict and most important pipeline components. + +Here, %mddoclink(api,pipeline.service.service,Service) +class, that can be used for pre- and postprocessing of messages is shown. + +Pipeline's %mddoclink(api,pipeline.pipeline.pipeline,Pipeline.from_dict) +static method is used for pipeline creation (from dictionary). +""" + +# %pip install dff + +# %% +import logging + +from dff.pipeline import Service, Pipeline, ACTOR + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + + +# %% [markdown] +""" +When Pipeline is created using `from_dict` method, +pipeline should be defined as a dictionary. +It should contain `services` - a `ServiceGroupBuilder` object, +basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects, +see tutorial 4. + +On pipeline execution services from `services` +list are run without difference between pre- and postprocessors. +Actor constant "ACTOR" should also be present among services. +ServiceBuilder object can be defined either with callable +(see tutorial 2) or with dict / object. +It should contain `handler` - a ServiceBuilder object. + +Not only Pipeline can be run using `__call__` method, +for most cases `run` method should be used. +It starts pipeline asynchronously and connects to provided messenger interface. + +Here pipeline contains 4 services, +defined in 4 different ways with different signatures. +""" + + +# %% +def prepreprocess(_): + logger.info( + "preprocession intent-detection Service running (defined as a dict)" + ) + + +def preprocess(_): + logger.info( + "another preprocession web-based annotator Service " + "(defined as a callable)" + ) + + +def postprocess(_): + logger.info("postprocession Service (defined as an object)") + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + { + "handler": prepreprocess, + }, + preprocess, + ACTOR, + Service( + handler=postprocess, + ), + ], +} + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) # This runs tutorial in interactive mode diff --git a/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.py.txt b/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.py.txt new file mode 100644 index 0000000000..91c29dbded --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.3_pipeline_dict_with_services_full.py.txt @@ -0,0 +1,167 @@ +# %% [markdown] +""" +# 3. Pipeline dict with services (full) + +The following tutorial shows `pipeline` creation from dict +and most important pipeline components. + +This tutorial is a more advanced version of the +[previous tutorial](%doclink(tutorial,pipeline.3_pipeline_dict_with_services_basic)). +""" + +# %pip install dff + +# %% +import json +import logging +import urllib.request + +from dff.script import Context +from dff.messengers.common import CLIMessengerInterface +from dff.pipeline import Service, Pipeline, ServiceRuntimeInfo, ACTOR +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH + +logger = logging.getLogger(__name__) + + +# %% [markdown] +""" +When Pipeline is created using `from_dict` method, +pipeline should be defined as `PipelineBuilder` objects +(defined in `types` module). +These objects are dictionaries of particular structure: + +* `messenger_interface` - `MessengerInterface` instance, + is used to connect to channel and transfer IO to user. +* `context_storage` - Place to store dialog contexts + (dictionary or a `DBContextStorage` instance). +* `services` (required) - A `ServiceGroupBuilder` object, + basically a list of `ServiceBuilder` or `ServiceGroupBuilder` objects, + see tutorial 4. +* `wrappers` - A list of pipeline wrappers, see tutorial 7. +* `timeout` - Pipeline timeout, see tutorial 5. +* `optimization_warnings` - Whether pipeline asynchronous structure + should be checked during initialization, + see tutorial 5. + +On pipeline execution services from `services` list are run +without difference between pre- and postprocessors. +If "ACTOR" constant is not found among `services` pipeline creation fails. +There can be only one "ACTOR" constant in the pipeline. +ServiceBuilder object can be defined either with callable (see tutorial 2) or +with dict of structure / object with following constructor arguments: + +* `handler` (required) - ServiceBuilder, + if handler is an object or a dict itself, + it will be used instead of base ServiceBuilder. + NB! Fields of nested ServiceBuilder will be overridden + by defined fields of the base ServiceBuilder. +* `wrappers` - a list of service wrappers, see tutorial 7. +* `timeout` - service timeout, see tutorial 5. +* `asynchronous` - whether or not this service _should_ be asynchronous + (keep in mind that not all services _can_ be asynchronous), + see tutorial 5. +* `start_condition` - service start condition, see tutorial 4. +* `name` - custom defined name for the service + (keep in mind that names in one ServiceGroup should be unique), + see tutorial 4. + +Not only Pipeline can be run using `__call__` method, +for most cases `run` method should be used. +It starts pipeline asynchronously and connects to provided messenger interface. + +Here pipeline contains 4 services, +defined in 4 different ways with different signatures. +First two of them write sample feature detection data to `ctx.misc`. +The first uses a constant expression and the second fetches from `example.com`. +Third one is "ACTOR" constant (it acts like a _special_ service here). +Final service logs `ctx.misc` dict. +""" + + +# %% +def prepreprocess(ctx: Context): + logger.info( + "preprocession intent-detection Service running (defined as a dict)" + ) + ctx.misc["preprocess_detection"] = { + ctx.last_request.text: "some_intent" + } # Similar syntax can be used to access + # service output dedicated to current pipeline run + + +def preprocess(ctx: Context, _, info: ServiceRuntimeInfo): + logger.info( + f"another preprocession web-based annotator Service" + f"(defined as a callable), named '{info.name}'" + ) + with urllib.request.urlopen("https://example.com/") as webpage: + web_content = webpage.read().decode( + webpage.headers.get_content_charset() + ) + ctx.misc["another_detection"] = { + ctx.last_request.text: "online" + if "Example Domain" in web_content + else "offline" + } + + +def postprocess(ctx: Context, pl: Pipeline): + logger.info("postprocession Service (defined as an object)") + logger.info( + f"resulting misc looks like:" + f"{json.dumps(ctx.misc, indent=4, default=str)}" + ) + fallback_flow, fallback_node, _ = pl.actor.fallback_label + received_response = pl.script[fallback_flow][fallback_node].response + responses_match = received_response == ctx.last_response + logger.info(f"actor is{'' if responses_match else ' not'} in fallback node") + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "messenger_interface": CLIMessengerInterface( + intro="Hi, this is a brand new Pipeline running!", + prompt_request="Request: ", + prompt_response="Response: ", + ), # `CLIMessengerInterface` has the following constructor parameters: + # `intro` - a string that will be displayed + # on connection to interface (on `pipeline.run`) + # `prompt_request` - a string that will be displayed before user input + # `prompt_response` - an output prefix string + "context_storage": {}, + "components": [ + { + "handler": { + "handler": prepreprocess, + "name": "silly_service_name", + }, + "name": "preprocessor", + }, # This service will be named `preprocessor` + # handler name will be overridden + preprocess, + ACTOR, + Service( + handler=postprocess, + name="postprocessor", + ), + ], +} + + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.py.txt b/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.py.txt new file mode 100644 index 0000000000..18b4527b5c --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_basic.py.txt @@ -0,0 +1,125 @@ +# %% [markdown] +""" +# 4. Groups and conditions (basic) + +The following example shows `pipeline` service group usage and start conditions. + +Here, %mddoclink(api,pipeline.service.service,Service)s +and %mddoclink(api,pipeline.service.group,ServiceGroup)s +are shown for advanced data pre- and postprocessing based on conditions. +""" + +# %pip install dff + +# %% +import json +import logging + +from dff.pipeline import ( + Service, + Pipeline, + not_condition, + service_successful_condition, + ServiceRuntimeInfo, + ACTOR, +) + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + + +# %% [markdown] +""" +Pipeline can contain not only single services, but also service groups. +Service groups can be defined as `ServiceGroupBuilder` objects: + lists of `ServiceBuilders` and `ServiceGroupBuilders` or objects. +The objects should contain `services` - +a ServiceBuilder and ServiceGroupBuilder object list. + +To receive serialized information about service, + service group or pipeline a property `info_dict` can be used, + it returns important object properties as a dict. + +Services and service groups can be executed conditionally. +Conditions are functions passed to `start_condition` argument. +These functions should have following signature: + + (ctx: Context, pipeline: Pipeline) -> bool. + +Service is only executed if its start_condition returned `True`. +By default all the services start unconditionally. +There are number of built-in condition functions. +Built-in condition functions check other service states. +These are most important built-in condition functions: + +* `always_start_condition` - Default condition function, always starts service. +* `service_successful_condition(path)` - Function that checks, + whether service with given `path` executed successfully. +* `not_condition(function)` - Function that returns result + opposite from the one returned + by the `function` (condition function) argument. + +Here there is a conditionally executed service named +`never_running_service` is always executed. +It is executed only if `always_running_service` +is not finished, that should never happen. +The service named `context_printing_service` +prints pipeline runtime information, +that contains execution state of all previously run services. +""" + + +# %% +def always_running_service(_, __, info: ServiceRuntimeInfo): + logger.info(f"Service '{info.name}' is running...") + + +def never_running_service(_, __, info: ServiceRuntimeInfo): + raise Exception(f"Oh no! The '{info.name}' service is running!") + + +def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): + logger.info( + f"Service '{info.name}' runtime execution info:" + f"{json.dumps(info, indent=4, default=str)}" + ) + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + Service( + handler=always_running_service, + name="always_running_service", + ), + ACTOR, + Service( + handler=never_running_service, + start_condition=not_condition( + service_successful_condition(".pipeline.always_running_service") + ), + ), + Service( + handler=runtime_info_printing_service, + name="runtime_info_printing_service", + ), + ], +} + + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_full.py.txt b/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_full.py.txt new file mode 100644 index 0000000000..fcd6ef5ae0 --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.4_groups_and_conditions_full.py.txt @@ -0,0 +1,214 @@ +# %% [markdown] +""" +# 4. Groups and conditions (full) + +The following tutorial shows `pipeline` service group usage and start conditions. + +This tutorial is a more advanced version of the +[previous tutorial](%doclink(tutorial,pipeline.4_groups_and_conditions_basic)). +""" + +# %pip install dff + +# %% +import logging + +from dff.pipeline import ( + Service, + Pipeline, + ServiceGroup, + not_condition, + service_successful_condition, + all_condition, + ServiceRuntimeInfo, + ACTOR, +) + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + + +# %% [markdown] +""" +Pipeline can contain not only single services, but also service groups. +Service groups can be defined as lists of `ServiceBuilders` + (in fact, all of the pipeline services are combined + into root service group named "pipeline"). +Alternatively, the groups can be defined as objects + with following constructor arguments: + +* `components` (required) - A list of ServiceBuilder objects, + ServiceGroup objects and lists of them. +* `wrappers` - A list of pipeline wrappers, see tutorial 7. +* `timeout` - Pipeline timeout, see tutorial 5. +* `asynchronous` - Whether or not this service group _should_ be asynchronous + (keep in mind that not all service groups _can_ be asynchronous), + see tutorial 5. +* `start_condition` - Service group start condition. +* `name` - Custom defined name for the service group + (keep in mind that names in one ServiceGroup should be unique). + +Service (and service group) object fields +are mostly the same as constructor parameters, +however there are some differences: + +* `requested_async_flag` - Contains the value received + from `asynchronous` constructor parameter. +* `calculated_async_flag` - Contains automatically calculated + possibility of the service to be asynchronous. +* `asynchronous` - Combination af `..._async_flag` fields, + requested value overrides calculated (if not `None`), + see tutorial 5. +* `path` - Contains globally unique (for pipeline) + path to the service or service group. + +If no name is specified for a service or service group, + the name will be generated according to the following rules: + +1. If service's handler is an Actor, service will be named 'actor'. +2. If service's handler is callable, + service will be named callable. +3. Service group will be named 'service_group'. +4. Otherwise, it will be named 'noname_service'. +5. After that an index will be added to service name. + +To receive serialized information about service, service group +or pipeline a property `info_dict` can be used, +it returns important object properties as a dict. +In addition to that `pretty_format` method of Pipeline +can be used to get all pipeline properties as a formatted string +(e.g. for logging or debugging purposes). + +Services and service groups can be executed conditionally. +Conditions are functions passed to `start_condition` argument. +These functions should have following signature: + + (ctx: Context, pipeline: Pipeline) -> bool. + +Service is only executed if its start_condition returned `True`. +By default all the services start unconditionally. +There are number of built-in condition functions as well +as possibility to create custom ones. +Custom condition functions can rely on data in `ctx.misc` +as well as on any external data source. +Built-in condition functions check other service states. +All of the services store their execution status in context, + this status can be one of the following: + +* `NOT_RUN` - Service hasn't bee executed yet. +* `RUNNING` - Service is currently being executed + (important for asynchronous services). +* `FINISHED` - Service finished successfully. +* `FAILED` - Service execution failed (that also throws an exception). + +There are following built-in condition functions: + +* `always_start_condition` - Default condition function, + always starts service. +* `service_successful_condition(path)` - + Function that checks, whether service + with given `path` executed successfully (is `FINISHED`). +* `not_condition(function)` - + Function that returns result opposite + from the one returned by + the `function` (condition function) argument. +* `aggregate_condition(aggregator, *functions)` - + Function that aggregated results of + numerous `functions` (condition functions) + using special `aggregator` function. +* `all_condition(*functions)` - + Function that returns True only if all + of the given `functions` + (condition functions) return `True`. +* `any_condition(*functions)` - + Function that returns `True` + if any of the given `functions` + (condition functions) return `True`. +NB! Actor service ALWAYS runs unconditionally. + +Here there are two conditionally executed services: +a service named `running_service` is executed + only if both `simple_services` in `service_group_0` + are finished successfully. +`never_running_service` is executed only if `running_service` is not finished, +this should never happen. +`context_printing_service` prints pipeline runtime information, + that contains execution state of all previously run services. +""" + + +# %% +def simple_service(_, __, info: ServiceRuntimeInfo): + logger.info(f"Service '{info.name}' is running...") + + +def never_running_service(_, __, info: ServiceRuntimeInfo): + raise Exception(f"Oh no! The '{info.name}' service is running!") + + +def runtime_info_printing_service(_, __, info: ServiceRuntimeInfo): + logger.info( + f"Service '{info.name}' runtime execution info:" + f"{info.model_dump_json(indent=4, default=str)}" + ) + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + [ + simple_service, # This simple service + # will be named `simple_service_0` + simple_service, # This simple service + # will be named `simple_service_1` + ], # Despite this is the unnamed service group in the root + # service group, it will be named `service_group_0` + ACTOR, + ServiceGroup( + name="named_group", + components=[ + Service( + handler=simple_service, + start_condition=all_condition( + service_successful_condition( + ".pipeline.service_group_0.simple_service_0" + ), + service_successful_condition( + ".pipeline.service_group_0.simple_service_1" + ), + ), # Alternative: + # service_successful_condition(".pipeline.service_group_0") + name="running_service", + ), # This simple service will be named `running_service`, + # because its name is manually overridden + Service( + handler=never_running_service, + start_condition=not_condition( + service_successful_condition( + ".pipeline.named_group.running_service" + ) + ), + ), + ], + ), + runtime_info_printing_service, + ], +} + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + logger.info(f"Pipeline structure:\n{pipeline.pretty_format()}") + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.py.txt b/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.py.txt new file mode 100644 index 0000000000..4d25fa7b96 --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_basic.py.txt @@ -0,0 +1,65 @@ +# %% [markdown] +""" +# 5. Asynchronous groups and services (basic) + +The following tutorial shows `pipeline` asynchronous +service and service group usage. + +Here, %mddoclink(api,pipeline.service.group,ServiceGroup)s +are shown for advanced and asynchronous data pre- and postprocessing. +""" + +# %pip install dff + +# %% +import asyncio + +from dff.pipeline import Pipeline, ACTOR + +from dff.utils.testing.common import ( + is_interactive_mode, + check_happy_path, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +# %% [markdown] +""" +Services and service groups can be synchronous and asynchronous. +In synchronous service groups services are executed consequently. +In asynchronous service groups all services are executed simultaneously. + +Service can be asynchronous if its handler is an async function. +Service group can be asynchronous if all services +and service groups inside it are asynchronous. + +Here there is an asynchronous service group, that contains 10 services, +each of them should sleep for 0.01 of a second. +However, as the group is asynchronous, +it is being executed for 0.01 of a second in total. +Service group `pipeline` can't be asynchronous because `actor` is synchronous. +""" + + +# %% +async def time_consuming_service(_): + await asyncio.sleep(0.01) + + +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + [time_consuming_service for _ in range(0, 10)], + ACTOR, + ], +} + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.py.txt b/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.py.txt new file mode 100644 index 0000000000..137c029890 --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.5_asynchronous_groups_and_services_full.py.txt @@ -0,0 +1,153 @@ +# %% [markdown] +""" +# 5. Asynchronous groups and services (full) + +The following tutorial shows `pipeline` +asynchronous service and service group usage. + +This tutorial is a more advanced version of the +[previous tutorial](%doclink(tutorial,pipeline.5_asynchronous_groups_and_services_basic)). +""" + +# %pip install dff + +# %% +import asyncio +import json +import logging +import urllib.request + +from dff.script import Context + +from dff.pipeline import ServiceGroup, Pipeline, ServiceRuntimeInfo, ACTOR + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + +# %% [markdown] +""" +Services and service groups can be synchronous and asynchronous. +In synchronous service groups services are executed consequently, + some of them (`ACTOR`) can even return `Context` object, + modifying it. +In asynchronous service groups all services + are executed simultaneously and should not return anything, + neither modify Context. + +To become asynchronous service or service group + should _be able_ to be asynchronous + and should not be marked synchronous. +Service can be asynchronous if its handler is an async function. +Service group can be asynchronous if all services +and service groups inside it are asynchronous. +If service or service group can be asynchronous +the `asynchronous` constructor parameter is checked. +If the parameter is not set, +the service becomes asynchronous, and if set, it is used instead. +If service can not be asynchronous, +but is marked asynchronous, an exception is thrown. +NB! ACTOR service is always synchronous. + +The timeout field only works for asynchronous services and service groups. +If service execution takes more time than timeout, +it is aborted and marked as failed. + +Pipeline `optimization_warnings` argument can be used to + display optimization warnings during pipeline construction. +Generally for optimization purposes asynchronous + services should be combined into asynchronous + groups to run simultaneously. +Synchronous services should be expelled from (mostly) asynchronous groups. + +Here service group `balanced_group` can be asynchronous, + however it is requested to be synchronous, + so its services are executed consequently. +Service group `service_group_0` is asynchronous, + it doesn't run out of timeout of 0.02 seconds, + however contains 6 time consuming services, + each of them sleeps for 0.01 of a second. +Service group `service_group_1` is also asynchronous, +it logs HTTPS requests (from 1 to 15), + running simultaneously, in random order. +Service group `pipeline` can't be asynchronous because +`balanced_group` and ACTOR are synchronous. +""" + + +# %% +async def simple_asynchronous_service(_, __, info: ServiceRuntimeInfo): + logger.info(f"Service '{info.name}' is running") + + +async def time_consuming_service(_): + await asyncio.sleep(0.01) + + +def meta_web_querying_service( + photo_number: int, +): # This function returns services, a service factory + async def web_querying_service(ctx: Context, _, info: ServiceRuntimeInfo): + if ctx.misc.get("web_query", None) is None: + ctx.misc["web_query"] = {} + with urllib.request.urlopen( + f"https://jsonplaceholder.typicode.com/photos/{photo_number}" + ) as webpage: + web_content = webpage.read().decode( + webpage.headers.get_content_charset() + ) + ctx.misc["web_query"].update( + { + f"{ctx.last_request}" + f":photo_number_{photo_number}": json.loads(web_content)[ + "title" + ] + } + ) + logger.info(f"Service '{info.name}' has completed HTTPS request") + + return web_querying_service + + +def context_printing_service(ctx: Context): + logger.info(f"Context misc: {json.dumps(ctx.misc, indent=4, default=str)}") + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "optimization_warnings": True, + # There are no warnings - pipeline is well-optimized + "components": [ + ServiceGroup( + name="balanced_group", + asynchronous=False, + components=[ + simple_asynchronous_service, + ServiceGroup( + timeout=0.02, + components=[time_consuming_service for _ in range(0, 6)], + ), + simple_asynchronous_service, + ], + ), + ACTOR, + [meta_web_querying_service(photo) for photo in range(1, 16)], + context_printing_service, + ], +} + +# %% +pipeline = Pipeline.from_dict(pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.6_extra_handlers_basic.py.txt b/_sources/tutorials/tutorials.pipeline.6_extra_handlers_basic.py.txt new file mode 100644 index 0000000000..386dd2885c --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.6_extra_handlers_basic.py.txt @@ -0,0 +1,123 @@ +# %% [markdown] +""" +# 6. Extra Handlers (basic) + +The following tutorial shows extra handlers possibilities and use cases. + +Here, extra handlers %mddoclink(api,pipeline.service.extra,BeforeHandler) +and %mddoclink(api,pipeline.service.extra,AfterHandler) +are shown as additional means of data processing, attached to services. +""" + +# %pip install dff + +# %% +import asyncio +import json +import logging +import random +from datetime import datetime + +from dff.script import Context + +from dff.pipeline import Pipeline, ServiceGroup, ExtraHandlerRuntimeInfo, ACTOR + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + +# %% [markdown] +""" +Extra handlers are additional function + lists (before-functions and/or after-functions) + that can be added to any `pipeline` components (service and service groups). +Extra handlers main purpose should be service +and service groups statistics collection. +Extra handlers can be attached to pipeline component using +`before_handler` and `after_handler` constructor parameter. + +Here 5 `heavy_service`s are run in single asynchronous service group. +Each of them sleeps for random amount of seconds (between 0 and 0.05). +To each of them (as well as to group) + time measurement extra handler is attached, + that writes execution time to `ctx.misc`. +In the end `ctx.misc` is logged to info channel. +""" + + +# %% +def collect_timestamp_before(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + ctx.misc.update({f"{info.component.name}": datetime.now()}) + + +def collect_timestamp_after(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + ctx.misc.update( + { + f"{info.component.name}": datetime.now() + - ctx.misc[f"{info.component.name}"] + } + ) + + +async def heavy_service(_): + await asyncio.sleep(random.randint(0, 5) / 100) + + +def logging_service(ctx: Context): + logger.info(f"Context misc: {json.dumps(ctx.misc, indent=4, default=str)}") + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + ServiceGroup( + before_handler=[collect_timestamp_before], + after_handler=[collect_timestamp_after], + components=[ + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + { + "handler": heavy_service, + "before_handler": [collect_timestamp_before], + "after_handler": [collect_timestamp_after], + }, + ], + ), + ACTOR, + logging_service, + ], +} + +# %% +pipeline = Pipeline(**pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.6_extra_handlers_full.py.txt b/_sources/tutorials/tutorials.pipeline.6_extra_handlers_full.py.txt new file mode 100644 index 0000000000..3672f0f82b --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.6_extra_handlers_full.py.txt @@ -0,0 +1,192 @@ +# %% [markdown] +""" +# 6. Extra Handlers (full) + +The following tutorial shows extra handlers possibilities and use cases. + +This tutorial is a more advanced version of the +[previous tutorial](%doclink(tutorial,pipeline.6_extra_handlers_basic)). +""" + +# %pip install dff psutil + +# %% +import json +import logging +import random +from datetime import datetime + +import psutil +from dff.script import Context + +from dff.pipeline import ( + Pipeline, + ServiceGroup, + to_service, + ExtraHandlerRuntimeInfo, + ServiceRuntimeInfo, + ACTOR, +) + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + +# %% [markdown] +""" +Extra handlers are additional function lists + (before-functions and/or after-functions) + that can be added to any pipeline components (service and service groups). +Despite extra handlers can be used to prepare data for certain services, +that require some very special input type, + in most cases services should be preferred for that purpose. +Extra handlers can be asynchronous, +however there's no statistics that can be collected about them. +So their main purpose should be _really_ lightweight data conversion (etc.) + operations or service and service groups statistics collection. + +Extra handlers have the following constructor arguments / parameters: + +* `functions` - Functions that will be run. +* `timeout` - Timeout for that extra handler + (for asynchronous extra handlers only). +* `asynchronous` - Whether this extra handler should be asynchronous or not. +NB! Extra handlers don't have execution state, +so their names shouldn't appear in built-in condition functions. + +Extra handlers callable signature can be one of the following: +`[ctx]`, `[ctx, pipeline]` or `[ctx, pipeline, info]`, where: + +* `ctx` - `Context` of the current dialog. +* `pipeline` - The current pipeline. +* `info` - Dictionary, containing information about current extra handler + and pipeline execution state (see tutorial 4). + +Extra handlers can be attached to pipeline component in a few different ways: + +1. Directly in constructor - by adding extra handlers to + `before_handler` or `after_handler` constructor parameter. +2. (Services only) `to_service` decorator - + transforms function to service with extra handlers + from `before_handler` and `after_handler` arguments. + +Here 5 `heavy_service`s fill big amounts of memory with random numbers. +Their runtime stats are captured and displayed by extra services, +`time_measure_handler` measures time and +`ram_measure_handler` - allocated memory. +Another `time_measure_handler` measures total +amount of time taken by all of them (combined in service group). +`logging_service` logs stats, however it can use string arguments only, + so `json_encoder_handler` is applied to encode stats to JSON. +""" + + +# %% +def get_extra_handler_misc_field( + info: ExtraHandlerRuntimeInfo, postfix: str +) -> str: # This method calculates `misc` field name dedicated to extra handler + # based on its and its service name + return f"{info.component.name}-{postfix}" + + +def time_measure_before_handler(ctx, _, info): + ctx.misc.update( + {get_extra_handler_misc_field(info, "time"): datetime.now()} + ) + + +def time_measure_after_handler(ctx, _, info): + ctx.misc.update( + { + get_extra_handler_misc_field(info, "time"): datetime.now() + - ctx.misc[get_extra_handler_misc_field(info, "time")] + } + ) + + +def ram_measure_before_handler(ctx, _, info): + ctx.misc.update( + { + get_extra_handler_misc_field( + info, "ram" + ): psutil.virtual_memory().available + } + ) + + +def ram_measure_after_handler(ctx, _, info): + ctx.misc.update( + { + get_extra_handler_misc_field(info, "ram"): ctx.misc[ + get_extra_handler_misc_field(info, "ram") + ] + - psutil.virtual_memory().available + } + ) + + +def json_converter_before_handler(ctx, _, info): + ctx.misc.update( + { + get_extra_handler_misc_field(info, "str"): json.dumps( + ctx.misc, indent=4, default=str + ) + } + ) + + +def json_converter_after_handler(ctx, _, info): + ctx.misc.pop(get_extra_handler_misc_field(info, "str")) + + +memory_heap = dict() # This object plays part of some memory heap + + +# %% +@to_service( + before_handler=[time_measure_before_handler, ram_measure_before_handler], + after_handler=[time_measure_after_handler, ram_measure_after_handler], +) +def heavy_service(ctx: Context): + memory_heap[ctx.last_request.text] = [ + random.randint(0, num) for num in range(0, 1000) + ] + + +@to_service( + before_handler=[json_converter_before_handler], + after_handler=[json_converter_after_handler], +) +def logging_service(ctx: Context, _, info: ServiceRuntimeInfo): + str_misc = ctx.misc[f"{info.name}-str"] + assert isinstance(str_misc, str) + print(f"Stringified misc: {str_misc}") + + +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + ServiceGroup( + before_handler=[time_measure_before_handler], + after_handler=[time_measure_after_handler], + components=[heavy_service for _ in range(0, 5)], + ), + ACTOR, + logging_service, + ], +} + +# %% +pipeline = Pipeline(**pipeline_dict) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.py.txt b/_sources/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.py.txt new file mode 100644 index 0000000000..ca1d9a6526 --- /dev/null +++ b/_sources/tutorials/tutorials.pipeline.7_extra_handlers_and_extensions.py.txt @@ -0,0 +1,144 @@ +# %% [markdown] +""" +# 7. Extra Handlers and Extensions + +The following tutorial shows how pipeline can be extended +by global extra handlers and custom functions. + +Here, %mddoclink(api,pipeline.pipeline.pipeline,Pipeline.add_global_handler) +function is shown, that can be used to add extra handlers before +and/or after all pipeline services. +""" + +# %pip install dff + +# %% +import asyncio +import json +import logging +import random +from datetime import datetime + +from dff.pipeline import ( + Pipeline, + ComponentExecutionState, + GlobalExtraHandlerType, + ExtraHandlerRuntimeInfo, + ServiceRuntimeInfo, + ACTOR, +) + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) +from dff.utils.testing.toy_script import HAPPY_PATH, TOY_SCRIPT + +logger = logging.getLogger(__name__) + +# %% [markdown] +""" +Pipeline functionality can be extended by global extra handlers. +Global extra handlers are special extra handlers + that are called on some stages of pipeline execution. +There are 4 types of global extra handlers: + + * `BEFORE_ALL` is called before pipeline execution. + * `BEFORE` is called before each service and service group execution. + * `AFTER` is called after each service and service group execution. + * `AFTER_ALL` is called after pipeline execution. + +Global extra handlers have the same signature as regular extra handlers. +Actually `BEFORE_ALL` and `AFTER_ALL` + are attached to root service group named 'pipeline', + so they return its runtime info + +All extra handlers warnings (see tutorial 7) +are applicable to global extra handlers. +Pipeline `add_global_extra_handler` function is used to register + global extra handlers. It accepts following arguments: + +* `global_extra_handler_type` (required) - A `GlobalExtraHandlerType` instance, + indicates extra handler type to add. +* `extra_handler` (required) - The extra handler function itself. +* `whitelist` - An optional list of paths, if it's not `None` + the extra handlers will be applied to + specified pipeline components only. +* `blacklist` - An optional list of paths, if it's not `None` + the extra handlers will be applied to + all pipeline components except specified. + +Here basic functionality of `df-node-stats` library is emulated. +Information about pipeline component execution time and + result is collected and printed to info log after pipeline execution. +Pipeline consists of actor and 25 `long_service`s +that run random amount of time between 0 and 0.05 seconds. +""" + +# %% +start_times = dict() # Place to temporarily store service start times +pipeline_info = dict() # Pipeline information storage + + +def before_all(_, __, info: ExtraHandlerRuntimeInfo): + global start_times, pipeline_info + now = datetime.now() + pipeline_info = {"start_time": now} + start_times = {info.component.path: now} + + +def before(_, __, info: ExtraHandlerRuntimeInfo): + start_times.update({info.component.path: datetime.now()}) + + +def after(_, __, info: ExtraHandlerRuntimeInfo): + start_time = start_times[info.component.path] + pipeline_info.update( + { + f"{info.component.path}_duration": datetime.now() - start_time, + f"{info.component.path}_success": info.component.execution_state.get( + info.component.path, ComponentExecutionState.NOT_RUN + ), + } + ) + + +def after_all(_, __, info: ExtraHandlerRuntimeInfo): + pipeline_info.update( + {"total_time": datetime.now() - start_times[info.component.path]} + ) + logger.info( + f"Pipeline stats: {json.dumps(pipeline_info, indent=4, default=str)}" + ) + + +async def long_service(_, __, info: ServiceRuntimeInfo): + timeout = random.randint(0, 5) / 100 + logger.info(f"Service {info.name} is going to sleep for {timeout} seconds.") + await asyncio.sleep(timeout) + + +# %% +pipeline_dict = { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + [long_service for _ in range(0, 25)], + ACTOR, + ], +} + +# %% +pipeline = Pipeline(**pipeline_dict) + +pipeline.add_global_handler(GlobalExtraHandlerType.BEFORE_ALL, before_all) +pipeline.add_global_handler(GlobalExtraHandlerType.BEFORE, before) +pipeline.add_global_handler(GlobalExtraHandlerType.AFTER, after) +pipeline.add_global_handler(GlobalExtraHandlerType.AFTER_ALL, after_all) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.1_basics.py.txt b/_sources/tutorials/tutorials.script.core.1_basics.py.txt new file mode 100644 index 0000000000..a06ffb96de --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.1_basics.py.txt @@ -0,0 +1,167 @@ +# %% [markdown] +""" +# Core: 1. Basics + +This notebook shows basic tutorial of creating a simple dialog bot (agent). + +Here, basic usege of %mddoclink(api,pipeline.pipeline.pipeline,Pipeline) +primitive is shown: its' creation with +%mddoclink(api,pipeline.pipeline.pipeline,Pipeline.from_script) +and execution. + +Additionally, function %mddoclink(api,utils.testing.common,check_happy_path) +that can be used for Pipeline testing is presented. + +Let's do all the necessary imports from DFF: +""" + +# %pip install dff + +# %% +from dff.script import TRANSITIONS, RESPONSE, Message +from dff.pipeline import Pipeline +import dff.script.conditions as cnd + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% [markdown] +""" +First of all, to create a dialog agent, we need to create a dialog script. +Below script means a dialog script. +A script is a dictionary, where the keys are the names of the flows. +A script can contain multiple scripts, which is needed in order to divide +a dialog into sub-dialogs and process them separately. +For example, the separation can be tied to the topic of the dialog. +In this tutorial there is one flow called `greeting_flow`. + +Flow describes a sub-dialog using linked nodes. +Each node has the keywords `RESPONSE` and `TRANSITIONS`. + +* `RESPONSE` contains the response + that the agent will return from the current node. +* `TRANSITIONS` describes transitions from the + current node to another nodes. This is a dictionary, + where keys are names of the nodes and + values are conditions of transition to them. +""" + + +# %% +toy_script = { + "greeting_flow": { + "start_node": { # This is the initial node, + # it doesn't contain a `RESPONSE`. + RESPONSE: Message(), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + # If "Hi" == request of the user then we make the transition. + }, + "node1": { + RESPONSE: Message( + text="Hi, how are you?" + ), # When the agent enters node1, + # return "Hi, how are you?". + TRANSITIONS: { + "node2": cnd.exact_match(Message(text="I'm fine, how are you?")) + }, + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + "node3": cnd.exact_match( + Message(text="Let's talk about music.") + ) + }, + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about music now."), + TRANSITIONS: { + "node4": cnd.exact_match(Message(text="Ok, goodbye.")) + }, + }, + "node4": { + RESPONSE: Message(text="Bye"), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + "fallback_node": { + # We get to this node if the conditions + # for switching to other nodes are not performed. + RESPONSE: Message(text="Ooops"), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + } +} + + +happy_path = ( + ( + Message(text="Hi"), + Message(text="Hi, how are you?"), + ), # start_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="Bye")), # node3 -> node4 + (Message(text="Hi"), Message(text="Hi, how are you?")), # node4 -> node1 + (Message(text="stop"), Message(text="Ooops")), # node1 -> fallback_node + ( + Message(text="stop"), + Message(text="Ooops"), + ), # fallback_node -> fallback_node + ( + Message(text="Hi"), + Message(text="Hi, how are you?"), + ), # fallback_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="Bye")), # node3 -> node4 +) + + +# %% [markdown] +""" +A `Pipeline` is an object that processes user +inputs and returns responses. +To create the pipeline you need to pass the script (`toy_script`), +initial node (`start_label`) and +the node to which the default transition will take place +if none of the current conditions are met (`fallback_label`). +By default, if `fallback_label` is not set, +then its value becomes equal to `start_label`. +""" + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path( + pipeline, + happy_path, + ) # This is a function for automatic tutorial + # running (testing tutorial) with `happy_path`. + + # Run tutorial in interactive mode if not in IPython env + # and if `DISABLE_INTERACTIVE_MODE` is not set. + if is_interactive_mode(): + run_interactive_mode(pipeline) + # This runs tutorial in interactive mode. diff --git a/_sources/tutorials/tutorials.script.core.2_conditions.py.txt b/_sources/tutorials/tutorials.script.core.2_conditions.py.txt new file mode 100644 index 0000000000..d8838b9f49 --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.2_conditions.py.txt @@ -0,0 +1,228 @@ +# %% [markdown] +""" +# Core: 2. Conditions + +This tutorial shows different options for +setting transition conditions from one node to another. + +Here, [conditions](%doclink(api,script.conditions.std_conditions)) +for script transitions are shown. + +First of all, let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +import re + +from dff.script import Context, TRANSITIONS, RESPONSE, Message +import dff.script.conditions as cnd +from dff.pipeline import Pipeline + +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +# %% [markdown] +""" +The transition condition is set by the function. +If this function returns the value `True`, +then the actor performs the corresponding transition. +Actor is responsible for processing user input and determining +the appropriate response based on the current state of the conversation and the script. +See tutorial 1 of pipeline (pipeline/1_basics) to learn more about Actor. +Condition functions have signature + + def func(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool + +Out of the box `dff.script.conditions` offers the + following options for setting conditions: + +* `exact_match` returns `True` if the user's request completely + matches the value passed to the function. +* `regexp` returns `True` if the pattern matches the user's request, + while the user's request must be a string. + `regexp` has same signature as `re.compile` function. +* `aggregate` returns `bool` value as + a result after aggregate by `aggregate_func` + for input sequence of conditions. + `aggregate_func == any` by default. `aggregate` has alias `agg`. +* `any` returns `True` if one element of input sequence of conditions is `True`. + `any(input_sequence)` is equivalent to + `aggregate(input sequence, aggregate_func=any)`. +* `all` returns `True` if all elements of input + sequence of conditions are `True`. + `all(input_sequence)` is equivalent to + `aggregate(input sequence, aggregate_func=all)`. +* `negation` returns negation of passed function. `negation` has alias `neg`. +* `has_last_labels` covered in the following examples. +* `true` returns `True`. +* `false` returns `False`. + +For example function +``` +def always_true_condition(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> bool: + return True +``` +always returns `True` and `always_true_condition` function +is the same as `dff.script.conditions.std_conditions.true()`. + +The functions to be used in the `toy_script` are declared here. +""" + + +# %% +def hi_lower_case_condition(ctx: Context, _: Pipeline, *args, **kwargs) -> bool: + request = ctx.last_request + # Returns True if `hi` in both uppercase and lowercase + # letters is contained in the user request. + if request is None or request.text is None: + return False + return "hi" in request.text.lower() + + +def complex_user_answer_condition( + ctx: Context, _: Pipeline, *args, **kwargs +) -> bool: + request = ctx.last_request + # The user request can be anything. + if request is None or request.misc is None: + return False + return {"some_key": "some_value"} == request.misc + + +def predetermined_condition(condition: bool): + # Wrapper for internal condition function. + def internal_condition_function( + ctx: Context, _: Pipeline, *args, **kwargs + ) -> bool: + # It always returns `condition`. + return condition + + return internal_condition_function + + +# %% +toy_script = { + "greeting_flow": { + "start_node": { # This is the initial node, + # it doesn't contain a `RESPONSE`. + RESPONSE: Message(), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + # If "Hi" == request of user then we make the transition + }, + "node1": { + RESPONSE: Message(text="Hi, how are you?"), + TRANSITIONS: {"node2": cnd.regexp(r".*how are you", re.IGNORECASE)}, + # pattern matching (precompiled) + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + "node3": cnd.all( + [cnd.regexp(r"talk"), cnd.regexp(r"about.*music")] + ) + }, + # Mix sequence of conditions by `cnd.all`. + # `all` is alias `aggregate` with + # `aggregate_func` == `all`. + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about music now."), + TRANSITIONS: {"node4": cnd.regexp(re.compile(r"Ok, goodbye."))}, + # pattern matching by precompiled pattern + }, + "node4": { + RESPONSE: Message(text="bye"), + TRANSITIONS: { + "node1": cnd.any( + [ + hi_lower_case_condition, + cnd.exact_match(Message(text="hello")), + ] + ) + }, + # Mix sequence of conditions by `cnd.any`. + # `any` is alias `aggregate` with + # `aggregate_func` == `any`. + }, + "fallback_node": { # We get to this node + # if an error occurred while the agent was running. + RESPONSE: Message(text="Ooops"), + TRANSITIONS: { + "node1": complex_user_answer_condition, + # The user request can be more than just a string. + # First we will check returned value of + # `complex_user_answer_condition`. + # If the value is `True` then we will go to `node1`. + # If the value is `False` then we will check a result of + # `predetermined_condition(True)` for `fallback_node`. + "fallback_node": predetermined_condition( + True + ), # or you can use `cnd.true()` + # Last condition function will return + # `true` and will repeat `fallback_node` + # if `complex_user_answer_condition` return `false`. + }, + }, + } +} + +# testing +happy_path = ( + ( + Message(text="Hi"), + Message(text="Hi, how are you?"), + ), # start_node -> node1 + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="bye")), # node3 -> node4 + (Message(text="Hi"), Message(text="Hi, how are you?")), # node4 -> node1 + (Message(text="stop"), Message(text="Ooops")), # node1 -> fallback_node + ( + Message(text="one"), + Message(text="Ooops"), + ), # fallback_node -> fallback_node + ( + Message(text="help"), + Message(text="Ooops"), + ), # fallback_node -> fallback_node + ( + Message(text="nope"), + Message(text="Ooops"), + ), # fallback_node -> fallback_node + ( + Message(misc={"some_key": "some_value"}), + Message(text="Hi, how are you?"), + ), # fallback_node -> node1 + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="bye")), # node3 -> node4 +) + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.3_responses.py.txt b/_sources/tutorials/tutorials.script.core.3_responses.py.txt new file mode 100644 index 0000000000..6d42b3b2b6 --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.3_responses.py.txt @@ -0,0 +1,213 @@ +# %% [markdown] +""" +# Core: 3. Responses + +This tutorial shows different options for setting responses. + +Here, [responses](%doclink(api,script.responses.std_responses)) +that allow giving custom answers to users are shown. + +Let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +import re +import random + +from dff.script import TRANSITIONS, RESPONSE, Context, Message +import dff.script.responses as rsp +import dff.script.conditions as cnd + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% [markdown] +""" +The response can be set by Callable or *Message: + +* Callable objects. If the object is callable it must have a special signature: + + func(ctx: Context, pipeline: Pipeline, *args, **kwargs) -> Any + +* *Message objects. If the object is *Message + it will be returned by the agent as a response. + + +The functions to be used in the `toy_script` are declared here. +""" + + +# %% +def cannot_talk_about_topic_response( + ctx: Context, _: Pipeline, *args, **kwargs +) -> Message: + request = ctx.last_request + if request is None or request.text is None: + topic = None + else: + topic_pattern = re.compile(r"(.*talk about )(.*)\.") + topic = topic_pattern.findall(request.text) + topic = topic and topic[0] and topic[0][-1] + if topic: + return Message(text=f"Sorry, I can not talk about {topic} now.") + else: + return Message(text="Sorry, I can not talk about that now.") + + +def upper_case_response(response: Message): + # wrapper for internal response function + def func(_: Context, __: Pipeline, *args, **kwargs) -> Message: + if response.text is not None: + response.text = response.text.upper() + return response + + return func + + +def fallback_trace_response( + ctx: Context, _: Pipeline, *args, **kwargs +) -> Message: + return Message( + misc={ + "previous_node": list(ctx.labels.values())[-2], + "last_request": ctx.last_request, + } + ) + + +# %% +toy_script = { + "greeting_flow": { + "start_node": { # This is an initial node, + # it doesn't need a `RESPONSE`. + RESPONSE: Message(), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + # If "Hi" == request of user then we make the transition + }, + "node1": { + RESPONSE: rsp.choice( + [ + Message(text="Hi, what is up?"), + Message(text="Hello, how are you?"), + ] + ), + # Random choice from candidate list. + TRANSITIONS: { + "node2": cnd.exact_match(Message(text="I'm fine, how are you?")) + }, + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + "node3": cnd.exact_match( + Message(text="Let's talk about music.") + ) + }, + }, + "node3": { + RESPONSE: cannot_talk_about_topic_response, + TRANSITIONS: { + "node4": cnd.exact_match(Message(text="Ok, goodbye.")) + }, + }, + "node4": { + RESPONSE: upper_case_response(Message(text="bye")), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + "fallback_node": { # We get to this node + # if an error occurred while the agent was running. + RESPONSE: fallback_trace_response, + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + } +} + +# testing +happy_path = ( + ( + Message(text="Hi"), + Message(text="Hello, how are you?"), + ), # start_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="BYE")), # node3 -> node4 + (Message(text="Hi"), Message(text="Hi, what is up?")), # node4 -> node1 + ( + Message(text="stop"), + Message( + misc={ + "previous_node": ("greeting_flow", "node1"), + "last_request": Message(text="stop"), + } + ), + ), + # node1 -> fallback_node + ( + Message(text="one"), + Message( + misc={ + "previous_node": ("greeting_flow", "fallback_node"), + "last_request": Message(text="one"), + } + ), + ), # f_n->f_n + ( + Message(text="help"), + Message( + misc={ + "previous_node": ("greeting_flow", "fallback_node"), + "last_request": Message(text="help"), + } + ), + ), # f_n->f_n + ( + Message(text="nope"), + Message( + misc={ + "previous_node": ("greeting_flow", "fallback_node"), + "last_request": Message(text="nope"), + } + ), + ), # f_n->f_n + ( + Message(text="Hi"), + Message(text="Hello, how are you?"), + ), # fallback_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="BYE")), # node3 -> node4 +) + +# %% +random.seed(31415) # predestination of choice + + +pipeline = Pipeline.from_script( + toy_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.4_transitions.py.txt b/_sources/tutorials/tutorials.script.core.4_transitions.py.txt new file mode 100644 index 0000000000..1a1ece5386 --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.4_transitions.py.txt @@ -0,0 +1,309 @@ +# %% [markdown] +""" +# Core: 4. Transitions + +This tutorial shows settings for transitions between flows and nodes. + +Here, [conditions](%doclink(api,script.conditions.std_conditions)) +for transition between many different script steps are shown. + +Some of the destination steps can be set using +[labels](%doclink(api,script.labels.std_labels)). + +First of all, let's do all the necessary imports from DFF. +""" + + +# %pip install dff + +# %% +import re + +from dff.script import TRANSITIONS, RESPONSE, Context, NodeLabel3Type, Message +import dff.script.conditions as cnd +import dff.script.labels as lbl +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +# %% [markdown] +""" +Let's define the functions with a special type of return value: + + NodeLabel3Type == tuple[str, str, float] + +which means that transition returns a `tuple` +with flow name, node name and priority. +""" + + +# %% +def greeting_flow_n2_transition( + _: Context, __: Pipeline, *args, **kwargs +) -> NodeLabel3Type: + return ("greeting_flow", "node2", 1.0) + + +def high_priority_node_transition(flow_label, label): + def transition(_: Context, __: Pipeline, *args, **kwargs) -> NodeLabel3Type: + return (flow_label, label, 2.0) + + return transition + + +# %% [markdown] +""" +Priority is needed to select a condition +in the situation where more than one condition is `True`. +All conditions in `TRANSITIONS` are being checked. +Of the set of `True` conditions, +the one that has the highest priority will be executed. +Of the set of `True` conditions with largest +priority the first met condition will be executed. + +Out of the box `dff.script.core.labels` +offers the following methods: + +* `lbl.repeat()` returns transition handler + which returns `NodeLabelType` to the last node, + +* `lbl.previous()` returns transition handler + which returns `NodeLabelType` to the previous node, + +* `lbl.to_start()` returns transition handler + which returns `NodeLabelType` to the start node, + +* `lbl.to_fallback()` returns transition + handler which returns `NodeLabelType` to the fallback node, + +* `lbl.forward()` returns transition handler + which returns `NodeLabelType` to the forward node, + +* `lbl.backward()` returns transition handler + which returns `NodeLabelType` to the backward node. + +There are three flows here: `global_flow`, `greeting_flow`, `music_flow`. +""" + +# %% +toy_script = { + "global_flow": { + "start_node": { # This is an initial node, + # it doesn't need a `RESPONSE`. + RESPONSE: Message(), + TRANSITIONS: { + ("music_flow", "node1"): cnd.regexp( + r"talk about music" + ), # first check + ("greeting_flow", "node1"): cnd.regexp( + r"hi|hello", re.IGNORECASE + ), # second check + "fallback_node": cnd.true(), # third check + # "fallback_node" is equivalent to + # ("global_flow", "fallback_node"). + }, + }, + "fallback_node": { # We get to this node if + # an error occurred while the agent was running. + RESPONSE: Message(text="Ooops"), + TRANSITIONS: { + ("music_flow", "node1"): cnd.regexp( + r"talk about music" + ), # first check + ("greeting_flow", "node1"): cnd.regexp( + r"hi|hello", re.IGNORECASE + ), # second check + lbl.previous(): cnd.regexp( + r"previous", re.IGNORECASE + ), # third check + # lbl.previous() is equivalent + # to ("previous_flow", "previous_node") + lbl.repeat(): cnd.true(), # fourth check + # lbl.repeat() is equivalent to ("global_flow", "fallback_node") + }, + }, + }, + "greeting_flow": { + "node1": { + RESPONSE: Message(text="Hi, how are you?"), + # When the agent goes to node1, we return "Hi, how are you?" + TRANSITIONS: { + ( + "global_flow", + "fallback_node", + 0.1, + ): cnd.true(), # second check + "node2": cnd.regexp(r"how are you"), # first check + # "node2" is equivalent to ("greeting_flow", "node2", 1.0) + }, + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + lbl.to_fallback(0.1): cnd.true(), # third check + # lbl.to_fallback(0.1) is equivalent + # to ("global_flow", "fallback_node", 0.1) + lbl.forward(0.5): cnd.regexp(r"talk about"), # second check + # lbl.forward(0.5) is equivalent + # to ("greeting_flow", "node3", 0.5) + ("music_flow", "node1"): cnd.regexp( + r"talk about music" + ), # first check + # ("music_flow", "node1") is equivalent + # to ("music_flow", "node1", 1.0) + lbl.previous(): cnd.regexp( + r"previous", re.IGNORECASE + ), # third check + }, + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about that now."), + TRANSITIONS: {lbl.forward(): cnd.regexp(r"bye")}, + }, + "node4": { + RESPONSE: Message(text="Bye"), + TRANSITIONS: { + "node1": cnd.regexp(r"hi|hello", re.IGNORECASE), # first check + lbl.to_fallback(): cnd.true(), # second check + }, + }, + }, + "music_flow": { + "node1": { + RESPONSE: Message( + text="I love `System of a Down` group, " + "would you like to talk about it?" + ), + TRANSITIONS: { + lbl.forward(): cnd.regexp(r"yes|yep|ok", re.IGNORECASE), + lbl.to_fallback(): cnd.true(), + }, + }, + "node2": { + RESPONSE: Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + TRANSITIONS: { + lbl.forward(): cnd.regexp(r"next", re.IGNORECASE), + lbl.repeat(): cnd.regexp(r"repeat", re.IGNORECASE), + lbl.to_fallback(): cnd.true(), + }, + }, + "node3": { + RESPONSE: Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + TRANSITIONS: { + lbl.forward(): cnd.regexp(r"next", re.IGNORECASE), + lbl.backward(): cnd.regexp(r"back", re.IGNORECASE), + lbl.repeat(): cnd.regexp(r"repeat", re.IGNORECASE), + lbl.to_fallback(): cnd.true(), + }, + }, + "node4": { + RESPONSE: Message(text="That's all what I know."), + TRANSITIONS: { + greeting_flow_n2_transition: cnd.regexp( + r"next", re.IGNORECASE + ), # second check + high_priority_node_transition( + "greeting_flow", "node4" + ): cnd.regexp( + r"next time", re.IGNORECASE + ), # first check + lbl.to_fallback(): cnd.true(), # third check + }, + }, + }, +} + +# testing +happy_path = ( + (Message(text="hi"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="talk about music."), + Message( + text="I love `System of a Down` group, " + "would you like to talk about it?" + ), + ), + ( + Message(text="yes"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="next"), + Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + ), + ( + Message(text="back"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="repeat"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="next"), + Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + ), + (Message(text="next"), Message(text="That's all what I know.")), + ( + Message(text="next"), + Message(text="Good. What do you want to talk about?"), + ), + (Message(text="previous"), Message(text="That's all what I know.")), + (Message(text="next time"), Message(text="Bye")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="previous"), Message(text="Bye")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="nope"), Message(text="Ooops")), + (Message(text="hi"), Message(text="Hi, how are you?")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="previous"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="let's talk about something."), + Message(text="Sorry, I can not talk about that now."), + ), + (Message(text="Ok, goodbye."), Message(text="Bye")), +) + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("global_flow", "start_node"), + fallback_label=("global_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.5_global_transitions.py.txt b/_sources/tutorials/tutorials.script.core.5_global_transitions.py.txt new file mode 100644 index 0000000000..f0dc5a0fcc --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.5_global_transitions.py.txt @@ -0,0 +1,221 @@ +# %% [markdown] +""" +# Core: 5. Global transitions + +This tutorial shows the global setting of transitions. + +Here, global [conditions](%doclink(api,script.conditions.std_conditions)) +for default transition between many different script steps are shown. + +First of all, let's do all the necessary imports from DFF. +""" + + +# %pip install dff + +# %% +import re + +from dff.script import GLOBAL, TRANSITIONS, RESPONSE, Message +import dff.script.conditions as cnd +import dff.script.labels as lbl +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +# %% [markdown] +""" +The keyword `GLOBAL` is used to define a global node. +There can be only one global node in a script. +The value that corresponds to this key has the +`dict` type with the same keywords as regular nodes. +The global node is defined above the flow level as opposed to regular nodes. +This node allows to define default global values for all nodes. + +There are `GLOBAL` node and three flows: +`global_flow`, `greeting_flow`, `music_flow`. +""" + +# %% +toy_script = { + GLOBAL: { + TRANSITIONS: { + ("greeting_flow", "node1", 1.1): cnd.regexp( + r"\b(hi|hello)\b", re.I + ), # first check + ("music_flow", "node1", 1.1): cnd.regexp( + r"talk about music" + ), # second check + lbl.to_fallback(0.1): cnd.true(), # fifth check + lbl.forward(): cnd.all( + [ + cnd.regexp(r"next\b"), + cnd.has_last_labels( + labels=[("music_flow", i) for i in ["node2", "node3"]] + ), + ] # third check + ), + lbl.repeat(0.2): cnd.all( + [ + cnd.regexp(r"repeat", re.I), + cnd.negation( + cnd.has_last_labels(flow_labels=["global_flow"]) + ), + ] # fourth check + ), + } + }, + "global_flow": { + "start_node": { + RESPONSE: Message() + }, # This is an initial node, it doesn't need a `RESPONSE`. + "fallback_node": { # We get to this node + # if an error occurred while the agent was running. + RESPONSE: Message(text="Ooops"), + TRANSITIONS: {lbl.previous(): cnd.regexp(r"previous", re.I)}, + # lbl.previous() is equivalent to + # ("previous_flow", "previous_node", 1.0) + }, + }, + "greeting_flow": { + "node1": { + RESPONSE: Message(text="Hi, how are you?"), + TRANSITIONS: {"node2": cnd.regexp(r"how are you")}, + # "node2" is equivalent to ("greeting_flow", "node2", 1.0) + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + lbl.forward(0.5): cnd.regexp(r"talk about"), + # lbl.forward(0.5) is equivalent to + # ("greeting_flow", "node3", 0.5) + lbl.previous(): cnd.regexp(r"previous", re.I), + }, + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about that now."), + TRANSITIONS: {lbl.forward(): cnd.regexp(r"bye")}, + }, + "node4": {RESPONSE: Message(text="bye")}, + # Only the global transitions setting are used in this node. + }, + "music_flow": { + "node1": { + RESPONSE: Message( + text="I love `System of a Down` group, " + "would you like to talk about it?" + ), + TRANSITIONS: {lbl.forward(): cnd.regexp(r"yes|yep|ok", re.I)}, + }, + "node2": { + RESPONSE: Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ) + # Only the global transitions setting are used in this node. + }, + "node3": { + RESPONSE: Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + TRANSITIONS: {lbl.backward(): cnd.regexp(r"back", re.I)}, + }, + "node4": { + RESPONSE: Message(text="That's all what I know."), + TRANSITIONS: { + ("greeting_flow", "node4"): cnd.regexp(r"next time", re.I), + ("greeting_flow", "node2"): cnd.regexp(r"next", re.I), + }, + }, + }, +} + +# testing +happy_path = ( + (Message(text="hi"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="talk about music."), + Message( + text="I love `System of a Down` group, " + "would you like to talk about it?" + ), + ), + ( + Message(text="yes"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="next"), + Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + ), + ( + Message(text="back"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="repeat"), + Message( + text="System of a Down is " + "an Armenian-American heavy metal band formed in 1994." + ), + ), + ( + Message(text="next"), + Message( + text="The band achieved commercial success " + "with the release of five studio albums." + ), + ), + (Message(text="next"), Message(text="That's all what I know.")), + ( + Message(text="next"), + Message(text="Good. What do you want to talk about?"), + ), + (Message(text="previous"), Message(text="That's all what I know.")), + (Message(text="next time"), Message(text="bye")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="previous"), Message(text="bye")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="nope"), Message(text="Ooops")), + (Message(text="hi"), Message(text="Hi, how are you?")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="previous"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="let's talk about something."), + Message(text="Sorry, I can not talk about that now."), + ), + (Message(text="Ok, goodbye."), Message(text="bye")), +) + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("global_flow", "start_node"), + fallback_label=("global_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.6_context_serialization.py.txt b/_sources/tutorials/tutorials.script.core.6_context_serialization.py.txt new file mode 100644 index 0000000000..bcf6cb7e24 --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.6_context_serialization.py.txt @@ -0,0 +1,89 @@ +# %% [markdown] +""" +# Core: 6. Context serialization + +This tutorial shows context serialization. +First of all, let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +import logging + +from dff.script import TRANSITIONS, RESPONSE, Context, Message +import dff.script.conditions as cnd + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% [markdown] +""" +This function returns the user request number. +""" + + +# %% +def response_handler(ctx: Context, _: Pipeline, *args, **kwargs) -> Message: + return Message(text=f"answer {len(ctx.requests)}") + + +# %% +# a dialog script +toy_script = { + "flow_start": { + "node_start": { + RESPONSE: response_handler, + TRANSITIONS: {("flow_start", "node_start"): cnd.true()}, + } + } +} + +# testing +happy_path = ( + (Message(text="hi"), Message(text="answer 1")), + (Message(text="how are you?"), Message(text="answer 2")), + (Message(text="ok"), Message(text="answer 3")), + (Message(text="good"), Message(text="answer 4")), +) + +# %% [markdown] +""" +Draft function that performs serialization. +""" + + +# %% +def process_response(ctx: Context): + ctx_json = ctx.model_dump_json() + if isinstance(ctx_json, str): + logging.info("context serialized to json str") + else: + raise Exception(f"ctx={ctx_json} has to be serialized to json string") + + ctx_dict = ctx.model_dump() + if isinstance(ctx_dict, dict): + logging.info("context serialized to dict") + else: + raise Exception(f"ctx={ctx_dict} has to be serialized to dict") + + if not isinstance(ctx, Context): + raise Exception(f"ctx={ctx} has to have Context type") + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("flow_start", "node_start"), + post_services=[process_response], +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.7_pre_response_processing.py.txt b/_sources/tutorials/tutorials.script.core.7_pre_response_processing.py.txt new file mode 100644 index 0000000000..38269b6a65 --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.7_pre_response_processing.py.txt @@ -0,0 +1,135 @@ +# %% [markdown] +""" +# Core: 7. Pre-response processing + +This tutorial shows pre-response processing feature. + +Here, %mddoclink(api,script.core.keywords,Keywords.PRE_RESPONSE_PROCESSING) +is demonstrated which can be used for additional context processing before response handlers. + +There are also some other %mddoclink(api,script.core.keywords,Keywords) +worth attention used in this tutorial. + +First of all, let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +from dff.script import ( + GLOBAL, + LOCAL, + RESPONSE, + TRANSITIONS, + PRE_RESPONSE_PROCESSING, + Context, + Message, +) +import dff.script.labels as lbl +import dff.script.conditions as cnd + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +def add_prefix(prefix): + def add_prefix_processing( + ctx: Context, _: Pipeline, *args, **kwargs + ) -> Context: + processed_node = ctx.current_node + processed_node.response = Message( + text=f"{prefix}: {processed_node.response.text}" + ) + ctx.overwrite_current_node_in_processing(processed_node) + return ctx + + return add_prefix_processing + + +# %% [markdown] +""" +`PRE_RESPONSE_PROCESSING` is a keyword that +can be used in `GLOBAL`, `LOCAL` or nodes. +""" + + +# %% +toy_script = { + "root": { + "start": { + RESPONSE: Message(), + TRANSITIONS: {("flow", "step_0"): cnd.true()}, + }, + "fallback": {RESPONSE: Message(text="the end")}, + }, + GLOBAL: { + PRE_RESPONSE_PROCESSING: { + "proc_name_1": add_prefix("l1_global"), + "proc_name_2": add_prefix("l2_global"), + } + }, + "flow": { + LOCAL: { + PRE_RESPONSE_PROCESSING: { + "proc_name_2": add_prefix("l2_local"), + "proc_name_3": add_prefix("l3_local"), + } + }, + "step_0": { + RESPONSE: Message(text="first"), + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_1": { + PRE_RESPONSE_PROCESSING: {"proc_name_1": add_prefix("l1_step_1")}, + RESPONSE: Message(text="second"), + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_2": { + PRE_RESPONSE_PROCESSING: {"proc_name_2": add_prefix("l2_step_2")}, + RESPONSE: Message(text="third"), + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_3": { + PRE_RESPONSE_PROCESSING: {"proc_name_3": add_prefix("l3_step_3")}, + RESPONSE: Message(text="fourth"), + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_4": { + PRE_RESPONSE_PROCESSING: {"proc_name_4": add_prefix("l4_step_4")}, + RESPONSE: Message(text="fifth"), + TRANSITIONS: {"step_0": cnd.true()}, + }, + }, +} + + +# testing +happy_path = ( + (Message(), Message(text="l3_local: l2_local: l1_global: first")), + (Message(), Message(text="l3_local: l2_local: l1_step_1: second")), + (Message(), Message(text="l3_local: l2_step_2: l1_global: third")), + (Message(), Message(text="l3_step_3: l2_local: l1_global: fourth")), + ( + Message(), + Message(text="l4_step_4: l3_local: l2_local: l1_global: fifth"), + ), + (Message(), Message(text="l3_local: l2_local: l1_global: first")), +) + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.8_misc.py.txt b/_sources/tutorials/tutorials.script.core.8_misc.py.txt new file mode 100644 index 0000000000..ac5eb813dd --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.8_misc.py.txt @@ -0,0 +1,167 @@ +# %% [markdown] +""" +# Core: 8. Misc + +This tutorial shows `MISC` (miscellaneous) keyword usage. + +See %mddoclink(api,script.core.keywords,Keywords.MISC) +for more information. + +First of all, let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +from dff.script import ( + GLOBAL, + LOCAL, + RESPONSE, + TRANSITIONS, + MISC, + Context, + Message, +) +import dff.script.labels as lbl +import dff.script.conditions as cnd +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +def custom_response(ctx: Context, _: Pipeline, *args, **kwargs) -> Message: + if ctx.validation: + return Message() + current_node = ctx.current_node + return Message( + text=f"ctx.last_label={ctx.last_label}: " + f"current_node.misc={current_node.misc}" + ) + + +# %% +toy_script = { + "root": { + "start": { + RESPONSE: Message(), + TRANSITIONS: {("flow", "step_0"): cnd.true()}, + }, + "fallback": {RESPONSE: Message(text="the end")}, + }, + GLOBAL: { + MISC: { + "var1": "global_data", + "var2": "global_data", + "var3": "global_data", + } + }, + "flow": { + LOCAL: { + MISC: { + "var2": "rewrite_by_local", + "var3": "rewrite_by_local", + } + }, + "step_0": { + MISC: {"var3": "info_of_step_0"}, + RESPONSE: custom_response, + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_1": { + MISC: {"var3": "info_of_step_1"}, + RESPONSE: custom_response, + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_2": { + MISC: {"var3": "info_of_step_2"}, + RESPONSE: custom_response, + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_3": { + MISC: {"var3": "info_of_step_3"}, + RESPONSE: custom_response, + TRANSITIONS: {lbl.forward(): cnd.true()}, + }, + "step_4": { + MISC: {"var3": "info_of_step_4"}, + RESPONSE: custom_response, + TRANSITIONS: {"step_0": cnd.true()}, + }, + }, +} + + +# testing +happy_path = ( + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_0'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_0'}" + ), + ), + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_1'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_1'}" + ), + ), + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_2'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_2'}" + ), + ), + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_3'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_3'}" + ), + ), + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_4'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_4'}" + ), + ), + ( + Message(), + Message( + text="ctx.last_label=('flow', 'step_0'): current_node.misc=" + "{'var1': 'global_data', " + "'var2': 'rewrite_by_local', " + "'var3': 'info_of_step_0'}" + ), + ), +) + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.core.9_pre_transitions_processing.py.txt b/_sources/tutorials/tutorials.script.core.9_pre_transitions_processing.py.txt new file mode 100644 index 0000000000..b875b4eb0e --- /dev/null +++ b/_sources/tutorials/tutorials.script.core.9_pre_transitions_processing.py.txt @@ -0,0 +1,106 @@ +# %% [markdown] +""" +# Core: 9. Pre-transitions processing + +This tutorial shows pre-transitions processing feature. + +Here, %mddoclink(api,script.core.keywords,Keywords.PRE_TRANSITIONS_PROCESSING) +is demonstrated which can be used for additional context +processing before transitioning to the next step. + +First of all, let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% +from dff.script import ( + GLOBAL, + RESPONSE, + TRANSITIONS, + PRE_RESPONSE_PROCESSING, + PRE_TRANSITIONS_PROCESSING, + Context, + Message, +) +import dff.script.labels as lbl +import dff.script.conditions as cnd +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +def save_previous_node_response_to_ctx_processing( + ctx: Context, _: Pipeline, *args, **kwargs +) -> Context: + processed_node = ctx.current_node + ctx.misc["previous_node_response"] = processed_node.response + return ctx + + +def get_previous_node_response_for_response_processing( + ctx: Context, _: Pipeline, *args, **kwargs +) -> Context: + processed_node = ctx.current_node + processed_node.response = Message( + text=f"previous={ctx.misc['previous_node_response'].text}:" + f" current={processed_node.response.text}" + ) + ctx.overwrite_current_node_in_processing(processed_node) + return ctx + + +# %% +# a dialog script +toy_script = { + "root": { + "start": { + RESPONSE: Message(), + TRANSITIONS: {("flow", "step_0"): cnd.true()}, + }, + "fallback": {RESPONSE: Message(text="the end")}, + }, + GLOBAL: { + PRE_RESPONSE_PROCESSING: { + "proc_name_1": get_previous_node_response_for_response_processing + }, + PRE_TRANSITIONS_PROCESSING: { + "proc_name_1": save_previous_node_response_to_ctx_processing + }, + TRANSITIONS: {lbl.forward(0.1): cnd.true()}, + }, + "flow": { + "step_0": {RESPONSE: Message(text="first")}, + "step_1": {RESPONSE: Message(text="second")}, + "step_2": {RESPONSE: Message(text="third")}, + "step_3": {RESPONSE: Message(text="fourth")}, + "step_4": {RESPONSE: Message(text="fifth")}, + }, +} + + +# testing +happy_path = ( + (Message(text="1"), Message(text="previous=None: current=first")), + (Message(text="2"), Message(text="previous=first: current=second")), + (Message(text="3"), Message(text="previous=second: current=third")), + (Message(text="4"), Message(text="previous=third: current=fourth")), + (Message(text="5"), Message(text="previous=fourth: current=fifth")), +) + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.responses.1_basics.py.txt b/_sources/tutorials/tutorials.script.responses.1_basics.py.txt new file mode 100644 index 0000000000..e021a4a670 --- /dev/null +++ b/_sources/tutorials/tutorials.script.responses.1_basics.py.txt @@ -0,0 +1,110 @@ +# %% [markdown] +""" +# Responses: 1. Basics + +Here, the process of response forming is shown. +Special keywords %mddoclink(api,script.core.keywords,Keywords.RESPONSE) +and %mddoclink(api,script.core.keywords,Keywords.TRANSITIONS) +are used for that. +""" + +# %pip install dff + +# %% +from typing import NamedTuple + +from dff.script import Message +from dff.script.conditions import exact_match +from dff.script import RESPONSE, TRANSITIONS +from dff.pipeline import Pipeline +from dff.utils.testing import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +toy_script = { + "greeting_flow": { + "start_node": { + RESPONSE: Message(text=""), + TRANSITIONS: {"node1": exact_match(Message(text="Hi"))}, + }, + "node1": { + RESPONSE: Message(text="Hi, how are you?"), + TRANSITIONS: { + "node2": exact_match(Message(text="i'm fine, how are you?")) + }, + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + "node3": exact_match(Message(text="Let's talk about music.")) + }, + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about music now."), + TRANSITIONS: {"node4": exact_match(Message(text="Ok, goodbye."))}, + }, + "node4": { + RESPONSE: Message(text="bye"), + TRANSITIONS: {"node1": exact_match(Message(text="Hi"))}, + }, + "fallback_node": { + RESPONSE: Message(text="Ooops"), + TRANSITIONS: {"node1": exact_match(Message(text="Hi"))}, + }, + } +} + +happy_path = ( + (Message(text="Hi"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), + (Message(text="Ok, goodbye."), Message(text="bye")), + (Message(text="Hi"), Message(text="Hi, how are you?")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="stop"), Message(text="Ooops")), + (Message(text="Hi"), Message(text="Hi, how are you?")), + ( + Message(text="i'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about music now."), + ), + (Message(text="Ok, goodbye."), Message(text="bye")), +) + + +# %% +class CallbackRequest(NamedTuple): + payload: str + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path( + pipeline, + happy_path, + ) # This is a function for automatic tutorial running + # (testing) with `happy_path` + + # This runs tutorial in interactive mode if not in IPython env + # and if `DISABLE_INTERACTIVE_MODE` is not set + if is_interactive_mode(): + run_interactive_mode(pipeline) # This runs tutorial in interactive mode diff --git a/_sources/tutorials/tutorials.script.responses.2_buttons.py.txt b/_sources/tutorials/tutorials.script.responses.2_buttons.py.txt new file mode 100644 index 0000000000..ff6373c0ce --- /dev/null +++ b/_sources/tutorials/tutorials.script.responses.2_buttons.py.txt @@ -0,0 +1,259 @@ +# %% [markdown] +""" +# Responses: 2. Buttons + +In this tutorial %mddoclink(api,script.core.message,Button) +class is demonstrated. +Buttons are one of %mddoclink(api,script.core.message,Message) fields. +They can be attached to any message but will only work if the chosen +[messenger interface](%doclink(api,index_messenger_interfaces)) supports them. +""" + + +# %pip install dff + +# %% +import dff.script.conditions as cnd +import dff.script.labels as lbl +from dff.script import Context, TRANSITIONS, RESPONSE + +from dff.script.core.message import Button, Keyboard, Message +from dff.pipeline import Pipeline +from dff.utils.testing import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +def check_button_payload(value: str): + def payload_check_inner(ctx: Context, _: Pipeline): + if ctx.last_request.misc is not None: + return ctx.last_request.misc.get("payload") == value + else: + return False + + return payload_check_inner + + +# %% +toy_script = { + "root": { + "start": { + RESPONSE: Message(text=""), + TRANSITIONS: { + ("general", "question_1"): cnd.true(), + }, + }, + "fallback": {RESPONSE: Message(text="Finishing test")}, + }, + "general": { + "question_1": { + RESPONSE: Message( + **{ + "text": "Starting test! What's 2 + 2?" + " (type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="5", payload="5"), + Button(text="4", payload="4"), + ] + ), + }, + } + ), + TRANSITIONS: { + lbl.forward(): check_button_payload("4"), + ("general", "question_1"): check_button_payload("5"), + }, + }, + "question_2": { + RESPONSE: Message( + **{ + "text": "Next question: what's 6 * 8?" + " (type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="38", payload="38"), + Button(text="48", payload="48"), + ] + ), + }, + } + ), + TRANSITIONS: { + lbl.forward(): check_button_payload("48"), + ("general", "question_2"): check_button_payload("38"), + }, + }, + "question_3": { + RESPONSE: Message( + **{ + "text": "What's 114 + 115? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="229", payload="229"), + Button(text="283", payload="283"), + ] + ), + }, + } + ), + TRANSITIONS: { + lbl.forward(): check_button_payload("229"), + ("general", "question_3"): check_button_payload("283"), + }, + }, + "success": { + RESPONSE: Message(text="Success!"), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + }, +} + +happy_path = ( + ( + Message(text="Hi"), + Message( + **{ + "text": "Starting test! What's 2 + 2? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="5", payload="5"), + Button(text="4", payload="4"), + ] + ) + }, + } + ), + ), + ( + Message(text="0"), + Message( + **{ + "text": "Starting test! What's 2 + 2? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="5", payload="5"), + Button(text="4", payload="4"), + ] + ), + }, + } + ), + ), + ( + Message(text="1"), + Message( + **{ + "text": "Next question: what's 6 * 8? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="38", payload="38"), + Button(text="48", payload="48"), + ] + ), + }, + } + ), + ), + ( + Message(text="0"), + Message( + **{ + "text": "Next question: what's 6 * 8? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="38", payload="38"), + Button(text="48", payload="48"), + ] + ), + }, + } + ), + ), + ( + Message(text="1"), + Message( + **{ + "text": "What's 114 + 115? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="229", payload="229"), + Button(text="283", payload="283"), + ] + ), + }, + } + ), + ), + ( + Message(text="1"), + Message( + **{ + "text": "What's 114 + 115? " + "(type in the index of the correct option)", + "misc": { + "ui": Keyboard( + buttons=[ + Button(text="229", payload="229"), + Button(text="283", payload="283"), + ] + ), + }, + } + ), + ), + (Message(text="0"), Message(text="Success!")), + (Message(text="ok"), Message(text="Finishing test")), +) + + +def process_request(ctx: Context): + ui = ( + ctx.last_response + and ctx.last_response.misc + and ctx.last_response.misc.get("ui") + ) + if ui and ui.buttons: + try: + chosen_button = ui.buttons[int(ctx.last_request.text)] + except (IndexError, ValueError): + raise ValueError( + "Type in the index of the correct option " + "to choose from the buttons." + ) + ctx.last_request = Message(misc={"payload": chosen_button.payload}) + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), + pre_services=[process_request], +) + +if __name__ == "__main__": + check_happy_path( + pipeline, + happy_path, + ) # For response object with `happy_path` string comparing, + # a special `generics_comparer` comparator is used + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.responses.3_media.py.txt b/_sources/tutorials/tutorials.script.responses.3_media.py.txt new file mode 100644 index 0000000000..ad29dd0546 --- /dev/null +++ b/_sources/tutorials/tutorials.script.responses.3_media.py.txt @@ -0,0 +1,127 @@ +# %% [markdown] +""" +# Responses: 3. Media + +Here, %mddoclink(api,script.core.message,Attachments) class is shown. +Attachments can be used for attaching different media elements +(such as %mddoclink(api,script.core.message,Image), %mddoclink(api,script.core.message,Document) +or %mddoclink(api,script.core.message,Audio)). + +They can be attached to any message but will only work if the chosen +[messenger interface](%doclink(api,index_messenger_interfaces)) supports them. +""" + +# %pip install dff + +# %% +from dff.script import RESPONSE, TRANSITIONS +from dff.script.conditions import std_conditions as cnd + +from dff.script.core.message import Attachments, Image, Message + +from dff.pipeline import Pipeline +from dff.utils.testing import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +# %% +img_url = "https://www.python.org/static/img/python-logo.png" +toy_script = { + "root": { + "start": { + RESPONSE: Message(text=""), + TRANSITIONS: {("pics", "ask_picture"): cnd.true()}, + }, + "fallback": { + RESPONSE: Message( + text="Final node reached, send any message to restart." + ), + TRANSITIONS: {("pics", "ask_picture"): cnd.true()}, + }, + }, + "pics": { + "ask_picture": { + RESPONSE: Message(text="Please, send me a picture url"), + TRANSITIONS: { + ("pics", "send_one", 1.1): cnd.regexp(r"^http.+\.png$"), + ("pics", "send_many", 1.0): cnd.regexp( + f"{img_url} repeat 10 times" + ), + ("pics", "repeat", 0.9): cnd.true(), + }, + }, + "send_one": { + RESPONSE: Message( + text="here's my picture!", + attachments=Attachments(files=[Image(source=img_url)]), + ), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + "send_many": { + RESPONSE: Message( + text="Look at my pictures", + attachments=Attachments(files=[Image(source=img_url)] * 10), + ), + TRANSITIONS: {("root", "fallback"): cnd.true()}, + }, + "repeat": { + RESPONSE: Message( + text="I cannot find the picture. Please, try again." + ), + TRANSITIONS: { + ("pics", "send_one", 1.1): cnd.regexp(r"^http.+\.png$"), + ("pics", "send_many", 1.0): cnd.regexp( + r"^http.+\.png repeat 10 times" + ), + ("pics", "repeat", 0.9): cnd.true(), + }, + }, + }, +} + +happy_path = ( + (Message(text="Hi"), Message(text="Please, send me a picture url")), + ( + Message(text="no"), + Message(text="I cannot find the picture. Please, try again."), + ), + ( + Message(text=img_url), + Message( + text="here's my picture!", + attachments=Attachments(files=[Image(source=img_url)]), + ), + ), + ( + Message(text="ok"), + Message(text="Final node reached, send any message to restart."), + ), + (Message(text="ok"), Message(text="Please, send me a picture url")), + ( + Message(text=f"{img_url} repeat 10 times"), + Message( + text="Look at my pictures", + attachments=Attachments(files=[Image(source=img_url)] * 10), + ), + ), + ( + Message(text="ok"), + Message(text="Final node reached, send any message to restart."), + ), +) + + +# %% +pipeline = Pipeline.from_script( + toy_script, + start_label=("root", "start"), + fallback_label=("root", "fallback"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.script.responses.4_multi_message.py.txt b/_sources/tutorials/tutorials.script.responses.4_multi_message.py.txt new file mode 100644 index 0000000000..e2b7836758 --- /dev/null +++ b/_sources/tutorials/tutorials.script.responses.4_multi_message.py.txt @@ -0,0 +1,149 @@ +# %% [markdown] +""" +# Responses: 4. Multi Message + +This tutorial shows Multi Message usage. + +The %mddoclink(api,script.core.message,MultiMessage) represents a combination of several messages. + +Let's do all the necessary imports from DFF. +""" + +# %pip install dff + +# %% + +from dff.script import TRANSITIONS, RESPONSE, Message, MultiMessage +import dff.script.conditions as cnd + +from dff.pipeline import Pipeline +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +# %% +toy_script = { + "greeting_flow": { + "start_node": { # This is an initial node, + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + # If "Hi" == request of user then we make the transition + }, + "node1": { + RESPONSE: MultiMessage( + messages=[ + Message(text="Hi, what is up?", misc={"confidences": 0.85}), + Message( + text="Hello, how are you?", misc={"confidences": 0.9} + ), + ] + ), + TRANSITIONS: { + "node2": cnd.exact_match(Message(text="I'm fine, how are you?")) + }, + }, + "node2": { + RESPONSE: Message(text="Good. What do you want to talk about?"), + TRANSITIONS: { + "node3": cnd.exact_match( + Message(text="Let's talk about music.") + ) + }, + }, + "node3": { + RESPONSE: Message(text="Sorry, I can not talk about that now."), + TRANSITIONS: { + "node4": cnd.exact_match(Message(text="Ok, goodbye.")) + }, + }, + "node4": { + RESPONSE: Message(text="bye"), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + "fallback_node": { # We get to this node + # if an error occurred while the agent was running. + RESPONSE: Message(text="Ooops"), + TRANSITIONS: {"node1": cnd.exact_match(Message(text="Hi"))}, + }, + } +} + +# testing +happy_path = ( + ( + Message(text="Hi"), + MultiMessage( + messages=[ + Message(text="Hi, what is up?", misc={"confidences": 0.85}), + Message(text="Hello, how are you?", misc={"confidences": 0.9}), + ] + ), + ), # start_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about that now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="bye")), # node3 -> node4 + ( + Message(text="Hi"), + MultiMessage( + messages=[ + Message(text="Hi, what is up?", misc={"confidences": 0.85}), + Message(text="Hello, how are you?", misc={"confidences": 0.9}), + ] + ), + ), # node4 -> node1 + ( + Message(text="stop"), + Message(text="Ooops"), + ), + # node1 -> fallback_node + ( + Message(text="one"), + Message(text="Ooops"), + ), # f_n->f_n + ( + Message(text="help"), + Message(text="Ooops"), + ), # f_n->f_n + ( + Message(text="nope"), + Message(text="Ooops"), + ), # f_n->f_n + ( + Message(text="Hi"), + MultiMessage( + messages=[ + Message(text="Hi, what is up?", misc={"confidences": 0.85}), + Message(text="Hello, how are you?", misc={"confidences": 0.9}), + ] + ), + ), # fallback_node -> node1 + ( + Message(text="I'm fine, how are you?"), + Message(text="Good. What do you want to talk about?"), + ), # node1 -> node2 + ( + Message(text="Let's talk about music."), + Message(text="Sorry, I can not talk about that now."), + ), # node2 -> node3 + (Message(text="Ok, goodbye."), Message(text="bye")), # node3 -> node4 +) + +# %% + +pipeline = Pipeline.from_script( + toy_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), +) + +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.stats.1_extractor_functions.py.txt b/_sources/tutorials/tutorials.stats.1_extractor_functions.py.txt new file mode 100644 index 0000000000..981333a1d7 --- /dev/null +++ b/_sources/tutorials/tutorials.stats.1_extractor_functions.py.txt @@ -0,0 +1,136 @@ +# %% [markdown] +""" +# 1. Extractor Functions + +The following example covers the basics of using the `stats` module. + +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. You can find API reference for default extractors +[here](%doclink(api,stats.default_extractors)). + +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`. +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 +to Clickhouse or other OLAP storages. + +
+ +Both the Opentelemetry collector and the Clickhouse instance must be running +during statistics collection. If you cloned the DFF repo, launch them using `docker compose`: +```bash +docker compose --profile stats up +``` + +
+ +For more information on OpenTelemetry instrumentation, +refer to the body of this tutorial as well as [OpenTelemetry documentation]( +https://opentelemetry.io/docs/instrumentation/python/manual/ +). + +""" + +# %pip install dff[stats] + +# %% +import asyncio + +from dff.script import Context +from dff.pipeline import ( + Pipeline, + ACTOR, + Service, + ExtraHandlerRuntimeInfo, + to_service, +) +from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH +from dff.stats import OtelInstrumentor, default_extractors +from dff.utils.testing import is_interactive_mode, check_happy_path + + +# %% [markdown] +""" +The cells below configure log export with the help of OTLP instrumentation. + +* The initial step is to configure the export destination. +`from_url` method of the `OtelInstrumentor` class simplifies this task +allowing you to only pass the url of the OTLP Collector server. + +* Alternatively, you can use the utility functions provided by the `stats` module: +`set_logger_destination`, `set_tracer_destination`, or `set_meter_destination`. These accept +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 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. + +""" + + +# %% +dff_instrumentor = OtelInstrumentor.from_url("grpc://localhost:4317") +dff_instrumentor.instrument() + +# %% [markdown] +""" +The following cell shows a custom extractor function. The data obtained from +the context and the runtime information gets shaped as a dict and returned +from the function body. The `dff_instrumentor` decorator then ensures +that the output is logged by OpenTelemetry. + +""" + + +# %% +# decorated by an OTLP Instrumentor instance +@dff_instrumentor +async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + # extract the execution state of a target service + data = { + "execution_state": info.component.execution_state, + } + # return the state as an arbitrary dict for further logging + return data + + +# %% +# configure `get_service_state` to run after the `heavy_service` +@to_service(after_handler=[get_service_state]) +async def heavy_service(ctx: Context): + _ = ctx # get something from ctx if needed + await asyncio.sleep(0.02) + + +# %% +pipeline = Pipeline.from_dict( + { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + heavy_service, + Service( + handler=ACTOR, + after_handler=[default_extractors.get_current_label], + ), + ], + } +) + + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + pipeline.run() diff --git a/_sources/tutorials/tutorials.stats.2_pipeline_integration.py.txt b/_sources/tutorials/tutorials.stats.2_pipeline_integration.py.txt new file mode 100644 index 0000000000..cd042d11e6 --- /dev/null +++ b/_sources/tutorials/tutorials.stats.2_pipeline_integration.py.txt @@ -0,0 +1,136 @@ +# %% [markdown] +""" +# 2. Pipeline Integration + +In the DFF ecosystem, extractor functions act as regular extra handlers ( +[see the pipeline module documentation](%doclink(tutorial,pipeline.6_extra_handlers_basic)) +). +Hence, you can decorate any part of your pipeline, including services, +service groups and the pipeline as a whole, to obtain the statistics +specific for that component. Some examples of this functionality +are showcased in this tutorial. + +
+ +Both the Opentelemetry collector and the Clickhouse instance must be running +during statistics collection. If you cloned the DFF repo, launch them using `docker compose`: +```bash +docker compose --profile stats up +``` + +
+""" + +# %pip install dff[stats] + +# %% +import asyncio + +from dff.script import Context +from dff.pipeline import ( + Pipeline, + ACTOR, + Service, + ExtraHandlerRuntimeInfo, + ServiceGroup, + GlobalExtraHandlerType, +) +from dff.utils.testing.toy_script import TOY_SCRIPT, HAPPY_PATH +from dff.stats import ( + OtelInstrumentor, + set_logger_destination, + set_tracer_destination, +) +from dff.stats import OTLPLogExporter, OTLPSpanExporter +from dff.stats import default_extractors +from dff.utils.testing import is_interactive_mode, check_happy_path + +# %% +set_logger_destination(OTLPLogExporter("grpc://localhost:4317", insecure=True)) +set_tracer_destination(OTLPSpanExporter("grpc://localhost:4317", insecure=True)) +dff_instrumentor = OtelInstrumentor() +dff_instrumentor.instrument() + + +# example extractor function +@dff_instrumentor +async def get_service_state(ctx: Context, _, info: ExtraHandlerRuntimeInfo): + # extract execution state of service from info + data = { + "execution_state": info.component.execution_state, + } + # return a record to save into connected database + return data + + +# %% +# example service +async def heavy_service(ctx: Context): + _ = ctx # get something from ctx if needed + await asyncio.sleep(0.02) + + +# %% [markdown] +""" + +The many ways in which you can use extractor functions are shown in the following +pipeline definition. The functions are used to obtain statistics from respective components: + +* A service group of two `heavy_service` instances. +* An `Actor` service. +* The pipeline as a whole. + +As is the case with the regular extra handler functions, you can wire the extractors +to run either before or after the target service. As a result, you can compare +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( + { + "script": TOY_SCRIPT, + "start_label": ("greeting_flow", "start_node"), + "fallback_label": ("greeting_flow", "fallback_node"), + "components": [ + ServiceGroup( + before_handler=[default_extractors.get_timing_before], + after_handler=[ + get_service_state, + default_extractors.get_timing_after, + ], + components=[ + {"handler": heavy_service}, + {"handler": heavy_service}, + ], + ), + Service( + handler=ACTOR, + before_handler=[ + default_extractors.get_timing_before, + ], + after_handler=[ + get_service_state, + default_extractors.get_current_label, + default_extractors.get_timing_after, + ], + ), + ], + } +) +pipeline.add_global_handler( + GlobalExtraHandlerType.BEFORE_ALL, default_extractors.get_timing_before +) +pipeline.add_global_handler( + GlobalExtraHandlerType.AFTER_ALL, default_extractors.get_timing_after +) +pipeline.add_global_handler(GlobalExtraHandlerType.AFTER_ALL, get_service_state) + +if __name__ == "__main__": + check_happy_path(pipeline, HAPPY_PATH) + if is_interactive_mode(): + pipeline.run() diff --git a/_sources/tutorials/tutorials.utils.1_cache.py.txt b/_sources/tutorials/tutorials.utils.1_cache.py.txt new file mode 100644 index 0000000000..676f2f9e59 --- /dev/null +++ b/_sources/tutorials/tutorials.utils.1_cache.py.txt @@ -0,0 +1,77 @@ +# %% [markdown] +""" +# 1. Cache + +In this tutorial use of +%mddoclink(api,utils.turn_caching.singleton_turn_caching,cache) +function is demonstrated. + +This function is used a lot like `functools.cache` function and +helps by saving results of heavy function execution and avoiding recalculation. + +Caches are kept in a library-wide singleton +and are cleared at the end of each turn. +""" + +# %pip install dff + +# %% +from dff.script.conditions import true +from dff.script import Context, TRANSITIONS, RESPONSE, Message +from dff.script.labels import repeat +from dff.pipeline import Pipeline +from dff.utils.turn_caching import cache +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + + +external_data = {"counter": 0} + + +# %% +@cache +def cached_response(_): + """ + This function execution result will be saved + for any set of given argument(s). + If the function will be called again + with the same arguments it will prevent it from execution. + The cached values will be used instead. + The cache is stored in a library-wide singleton, + that is cleared in the end of execution of actor and/or pipeline. + """ + external_data["counter"] += 1 + return external_data["counter"] + + +def response(ctx: Context, _, *__, **___) -> Message: + if ctx.validation: + return Message() + return Message( + text=f"{cached_response(1)}-{cached_response(2)}-" + f"{cached_response(1)}-{cached_response(2)}" + ) + + +# %% +toy_script = { + "flow": {"node1": {TRANSITIONS: {repeat(): true()}, RESPONSE: response}} +} + +happy_path = ( + (Message(), Message(text="1-2-1-2")), + (Message(), Message(text="3-4-3-4")), + (Message(), Message(text="5-6-5-6")), +) + +pipeline = Pipeline.from_script(toy_script, start_label=("flow", "node1")) + + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/tutorials/tutorials.utils.2_lru_cache.py.txt b/_sources/tutorials/tutorials.utils.2_lru_cache.py.txt new file mode 100644 index 0000000000..b0fa4f0280 --- /dev/null +++ b/_sources/tutorials/tutorials.utils.2_lru_cache.py.txt @@ -0,0 +1,75 @@ +# %% [markdown] +""" +# 2. LRU Cache + +In this tutorial use of +%mddoclink(api,utils.turn_caching.singleton_turn_caching,lru_cache) +function is demonstrated. + +This function is used a lot like `functools.lru_cache` function and +helps by saving results of heavy function execution and avoiding recalculation. + +Caches are kept in a library-wide singleton +and are cleared at the end of each turn. + +Maximum size parameter limits the amount of function execution results cached. +""" + +# %pip install dff + +# %% +from dff.script.conditions import true +from dff.script import Context, TRANSITIONS, RESPONSE, Message +from dff.script.labels import repeat +from dff.pipeline import Pipeline +from dff.utils.turn_caching import lru_cache +from dff.utils.testing.common import ( + check_happy_path, + is_interactive_mode, + run_interactive_mode, +) + +external_data = {"counter": 0} + + +# %% +@lru_cache(maxsize=2) +def cached_response(_): + """ + This function will work exactly the same as the one from previous + tutorial with only one exception. + Only 2 results will be stored; + when the function will be executed with third arguments set, + the least recent result will be deleted. + """ + external_data["counter"] += 1 + return external_data["counter"] + + +def response(ctx: Context, _, *__, **___) -> Message: + if ctx.validation: + return Message() + return Message( + text=f"{cached_response(1)}-{cached_response(2)}-{cached_response(3)}-" + f"{cached_response(2)}-{cached_response(1)}" + ) + + +# %% +toy_script = { + "flow": {"node1": {TRANSITIONS: {repeat(): true()}, RESPONSE: response}} +} + +happy_path = ( + (Message(), Message(text="1-2-3-2-4")), + (Message(), Message(text="5-6-7-6-8")), + (Message(), Message(text="9-10-11-10-12")), +) + +pipeline = Pipeline.from_script(toy_script, start_label=("flow", "node1")) + +# %% +if __name__ == "__main__": + check_happy_path(pipeline, happy_path) + if is_interactive_mode(): + run_interactive_mode(pipeline) diff --git a/_sources/user_guides.rst.txt b/_sources/user_guides.rst.txt new file mode 100644 index 0000000000..0cb1a15316 --- /dev/null +++ b/_sources/user_guides.rst.txt @@ -0,0 +1,39 @@ +User guides +----------- + +:doc:`Basic concepts <./user_guides/basic_conceptions>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +: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>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``superset guide`` tutorial highlights the usage of Superset visualization tool +for exploring the telemetry data collected from your conversational services. +We show how to plug in the telemetry collection and configure the pre-built +Superset dashboard shipped with DFF. + +:doc:`Optimization guide <./user_guides/optimization_guide>` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``optimization guide`` demonstrates various tools provided by the library +that you can use to profile your conversational service, +and to locate and remove performance bottlenecks. + +.. toctree:: + :hidden: + + user_guides/basic_conceptions + user_guides/context_guide + user_guides/superset_guide + user_guides/optimization_guide diff --git a/_sources/user_guides/basic_conceptions.rst.txt b/_sources/user_guides/basic_conceptions.rst.txt new file mode 100644 index 0000000000..5344143e05 --- /dev/null +++ b/_sources/user_guides/basic_conceptions.rst.txt @@ -0,0 +1,388 @@ +Basic Concepts +-------------- + +Introduction +~~~~~~~~~~~~ + +The Dialog Flow Framework (DFF) is a modern tool for designing conversational services. + +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. + +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. + + +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 + +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 +========================================== + +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 + + If user writes "Hello!": + Respond with "Hi! Let's play ping-pong!" + + If user afterwards writes "Ping" or "ping" or "Ping!" or "ping!": + Respond with "Pong!" + Repeat this behaviour + + If user writes something else: + Respond with "That was against the rules" + Go to responding with "Hi! Let's play ping-pong!" if user writes anything + +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. + +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 = { + "greeting_flow": { + "start_node": { + RESPONSE: Message(), # the response of the initial node is skipped + TRANSITIONS: { + ("greeting_flow", "greeting_node"): + cnd.exact_match(Message(text="/start")), + }, + }, + "greeting_node": { + RESPONSE: Message(text="Hi!"), + TRANSITIONS: { + ("ping_pong_flow", "game_start_node"): + cnd.exact_match(Message(text="Hello!")) + } + }, + "fallback_node": { + RESPONSE: fallback_response, + TRANSITIONS: { + ("greeting_flow", "greeting_node"): cnd.true(), + }, + }, + }, + "ping_pong_flow": { + "game_start_node": { + RESPONSE: Message(text="Let's play ping-pong!"), + TRANSITIONS: { + ("ping_pong_flow", "response_node"): + cnd.exact_match(Message(text="Ping!")), + }, + }, + "response_node": { + RESPONSE: Message(text="Pong!"), + TRANSITIONS: { + ("ping_pong_flow", "response_node"): + cnd.exact_match(Message(text="Ping!")), + }, + }, + }, + } + + pipeline = Pipeline.from_script( + ping_pong_script, + start_label=("greeting_flow", "start_node"), + fallback_label=("greeting_flow", "fallback_node"), + ) + + if __name__ == "__main__": + pipeline.run() + +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. + +* ``ping_pong_script``: The dialog **script** mentioned above is a dictionary that has one or more + dialog flows as its values. + +* ``ping_pong_flow`` is the game emulation flow; it contains linked + conversation nodes and possibly some extra data, transitions, etc. + +* A node object is an atomic part of the script. + The required fields of a node object are ``RESPONSE`` and ``TRANSITIONS``. + +* The ``RESPONSE`` field specifies the response that the dialog agent gives to the user in the current turn. + +* 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. + +* ``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!". + +* ``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 + 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 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 <../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. + +* 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`_. + +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 + + 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} + } + +.. 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 a special fallback response depending on the situation. + """ + 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!") + +Testing and Debugging +~~~~~~~~~~~~~~~~~~~~~ + +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. + +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. + +.. code-block:: python + + happy_path = ( + (Message(text="/start"), Message(text="Hi!")), + (Message(text="Hello!"), Message(text="Let's play ping-pong!")), + (Message(text="Ping!"), Message(text="Pong!")) + ) + +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. + +.. code-block:: python + + from dff.utils.testing.common import check_happy_path + + check_happy_path(pipeline, happy_path) + +Monitoring and Analytics +~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +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:: + + More information is available in the respective `guide <../user_guides/superset_guide.html>`__. + +Iterative Improvement +~~~~~~~~~~~~~~~~~~~~~ + +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. + +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. + +Data Protection +~~~~~~~~~~~~~~~ + +Data protection is a critical consideration in bot development, especially when handling sensitive information. + +.. note:: + + 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. + +Documentation +~~~~~~~~~~~~~ + +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 + + 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 diff --git a/_sources/user_guides/context_guide.rst.txt b/_sources/user_guides/context_guide.rst.txt new file mode 100644 index 0000000000..4f3f2dbc75 --- /dev/null +++ b/_sources/user_guides/context_guide.rst.txt @@ -0,0 +1,250 @@ +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>`_) + +.. 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 +`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/_sources/user_guides/optimization_guide.rst.txt b/_sources/user_guides/optimization_guide.rst.txt new file mode 100644 index 0000000000..198e704022 --- /dev/null +++ b/_sources/user_guides/optimization_guide.rst.txt @@ -0,0 +1,107 @@ +Optimization Guide +------------------ + +Introduction +~~~~~~~~~~~~ + +When optimizing a dialog service to provide the best possible user experience, +it's essential to identify and address performance issues. +Similar to any complex system, a dialog service can have performance bottlenecks at various levels. +These bottlenecks can occur during I/O operations like receiving and sending messages, +as well as when synchronizing service states with a database. +As the number of callbacks in the script and pipeline increases, +the performance of DFF classes can degrade leading to longer response time. + +As a result, it becomes necessary to locate the part of the pipeline that is causing issues, so that +further optimization steps can be taken. DFF provides several tools that address the need for +profiling individual system components. This guide will walk you through the process +of using these tools in practice and optimizing the profiled application. + +Profiling with Locust testing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Locust `__ is a tool for load testing web applications that +simultaneously spawns several user agents that execute a pre-determined behavior +against the target application. Assuming that your pipeline is integrated into a web +server application, like Flask or FastAPI, that is not strongly impacted by the load, +the load testing reveals how well your pipeline would scale to a highly loaded environment. +Using this approach, you can also measure the scalability of each component in your pipeline, +if you take advantage of the Opentelemetry package bundled with the library (`stats` extra required) +as described below. + +Since Locust testing can only target web apps, +this approach only applies if you integrate your dialog pipeline into a web application. +The `FastAPI integration tutorial <../tutorials/tutorials.messengers.web_api_interface.1_fastapi.py>`_ +shows the most straightforward way to do this. +At this stage, you will also need to instrument the pipeline components that you want to additionally profile +using `extractor functions`. Put simply, you are decorating the components of the pipeline +with functions that can report their performance, e.g. their execution time or the CPU load. + +.. note:: + + You can get more info on how instrumentation is done and statistics are collected + in the `stats tutorial <../tutorials/tutorials.stats.1_extractor_functions.py>`__. + +When you are done setting up the instrumentation, you can launch the web server to accept connections from locust. + +The final step is to run a Locust file which will result in artificial load traffic being generated and sent to your server. +A Locust file is a script that implements the behavior of artificial users, +i.e. the requests to the server that will be made during testing. + +.. note:: + + An example Locust script along with instructions on how to run it can be found in the + `load testing tutorial <../tutorials/tutorials.messengers.web_api_interface.3_load_testing_with_locust.py>`_. + The simplest way, however, is to pass a locust file to the Python interpreter. + +Once Locust is running, you can access its GUI, where you can set the number of users to emulate. +After configuring this parameter, the active phase of testing will begin, +and the results will become accessible on an interactive dashboard. +These reported values include timing data, such as the average response time of your service, +allowing you to assess the performance's reasonableness and impact on user experience. + +The data provided by extractor functions will be available in the Clickhouse database; +you can view it using the Superset dashboard (see `instructions <./superset_guide.html>`__) +or analyze it with your own queries using the Clickhouse client. + +Profiling context storages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Benchmarking the performance of context storage is crucial to understanding +how different storage methods impact your dialog service's efficiency. +This process involves running tests to measure the speed and reliability of various context storage solutions. +Given the exact configuration of your system, one or the other database type may be performing more efficiently, +so you may prefer to change your database depending on the testing results. + +.. note:: + The exact instructions of how the testing can be carried out are available in the + `DB benchmarking tutorial <../tutorials/tutorials.context_storages.8_db_benchmarking.py>`__. + +Optimization techniques +~~~~~~~~~~~~~~~~~~~~~~~ + +Aside from choosing an appropriate database type, there exists a number of other recommendations +that may help you improve the efficiency of your service. + +* Firstly, follow the DRY principle not only with regard to your code, but also with regard to + computational operations. In other words, you have to make sure that your callback functions work only once + during a dialog turn and only when needed. E.g. you can take note of the `conditions` api available as a part + of the `Pipeline` module: while normally a pipeline service runs every turn, you can restrict it + to only run on turns when a particular condition is satisfied, greatly reducing + the number of performed actions (see the + `Groups and Conditions tutorial <../tutorials/tutorials.pipeline.4_groups_and_conditions_full.py>`__). + +* Using caching for resource-consuming callbacks and actions may also prove to be a helpful strategy. + In this manner, you can improve the computational efficiency of your pipeline, + while making very few changes to the code itself. DFF includes a caching mechanism + for response functions. However, the simplicity + of the DFF API makes it easy to integrate any custom caching solutions that you may come up with. + See the `Cache tutorial <../tutorials/tutorials.utils.1_cache.py>`__. + +* Finally, be mindful about the use of computationally expensive algorithms, like NLU classifiers + or LLM-based generative networks, since those require a great deal of time and resources + to produce an answer. In case you need to use one, take full advantage of caching along with + other means to relieve the computational load imposed by neural networks such as message queueing. + +.. + todo: add a link to a user guide about using message queueing. diff --git a/_sources/user_guides/superset_guide.rst.txt b/_sources/user_guides/superset_guide.rst.txt new file mode 100644 index 0000000000..a819fd5449 --- /dev/null +++ b/_sources/user_guides/superset_guide.rst.txt @@ -0,0 +1,191 @@ +Superset guide +--------------------- + +Description +~~~~~~~~~~~ + +| The Dialog Flow Stats module can be used to obtain and visualize usage statistics for your service. +| The module relies on several open source solutions that allow for data persistence and visualization + +* `Clickhouse `_ serves as an OLAP storage for data. +* Batching and preprocessing data is based on `OpenTelemetry protocol `_ and the `OpenTelemetry collector `_. +* Interactive visualization is powered by `Apache Superset `_. + +All the mentioned services are shipped as Docker containers, including a pre-built Superset image that ensures API compatibility. + +Collection procedure +~~~~~~~~~~~~~~~~~~~~ + +**Installation** + +.. code-block:: shell + :linenos: + + # clone the original repository to access the docker compose file + git clone https://github.com/deeppavlov/dialog_flow_framework.git + # install with the stats extra + cd dialog_flow_framework + pip install .[stats] + +**Launching services** + +.. code-block:: shell + :linenos: + + # clone the original repository to access the docker compose file + git clone https://github.com/deeppavlov/dialog_flow_framework.git + # launch the required services + cd dialog_flow_framework + docker compose --profile stats up + +**Collecting data** + +Collecting data is done by means of instrumenting your conversational service before you run it. +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 + + python utils/stats/sample_data_provider.py + +Displaying the data +~~~~~~~~~~~~~~~~~~~ + +In order to display the Superset dashboard, you should update the default configuration with the credentials of your database. +The configuration can be optionally saved as a zip archive for inspection / debug. + +You can set most of the configuration options using a YAML file. +The default example file can be found in the `tutorials/stats` directory: + +.. code-block:: yaml + :linenos: + + # tutorials/stats/example_config.yaml + db: + driver: clickhousedb+connect + name: test + user: username + host: clickhouse + port: 8123 + table: otel_logs + +The file can then be used to parametrize the configuration script. + +.. code-block:: shell + + dff.stats tutorials/stats/example_config.yaml -P superset -dP pass -U superset --outfile=config_artifact.zip + +.. warning:: + + Here we passed passwords via CLI, which is not recommended. For enhanced security, call the command above omitting the passwords (`dff.stats -P -dP -U superset ...`) and you will be prompted to enter them interactively. + +Running the command will automatically import the dashboard as well as the data sources +into the running superset server. If you are using a version of Superset different from the one +shipped with DFF, make sure that your access rights are sufficient to edit the workspace. + +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 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. + +.. figure:: ../_static/images/overview.png + + Overview plots. + +* 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 + + Node stats plots. + +* General service load data aggregated over time can be found in the **Service stats** section. + +.. figure:: ../_static/images/service_stats.png + + 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. + +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/_static/auto-render.min.js b/_static/auto-render.min.js new file mode 100644 index 0000000000..c169ec639f --- /dev/null +++ b/_static/auto-render.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var a=r[e];if(void 0!==a)return a.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,n),i.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var a={};return function(){n.d(a,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,a=0,i=e.length;n0&&(a.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=i.test(d)?d:e.slice(t[l].left.length,n);a.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&a.push({type:"text",data:e}),a},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var a=document.createDocumentFragment(),i=0;i tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/binder_badge_logo.svg b/_static/binder_badge_logo.svg new file mode 100644 index 0000000000..327f6b639a --- /dev/null +++ b/_static/binder_badge_logo.svg @@ -0,0 +1 @@ + launchlaunchbinderbinder \ No newline at end of file diff --git a/_static/broken_example.png b/_static/broken_example.png new file mode 100644 index 0000000000..4fea24e7df Binary files /dev/null and b/_static/broken_example.png differ diff --git a/_static/check-solid.svg b/_static/check-solid.svg new file mode 100644 index 0000000000..92fad4b5c0 --- /dev/null +++ b/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js new file mode 100644 index 0000000000..54b3c46381 --- /dev/null +++ b/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/_static/copybutton.css b/_static/copybutton.css new file mode 100644 index 0000000000..f1916ec7d1 --- /dev/null +++ b/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 0000000000..2ea7ff3e21 --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 0000000000..dbe1aaad79 --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/css/custom.css b/_static/css/custom.css new file mode 100644 index 0000000000..4957fe1a45 --- /dev/null +++ b/_static/css/custom.css @@ -0,0 +1,45 @@ +.call-to-action-any { + width: 2rem; + height: 2rem; + margin-inline-end: 10px; + background-size: 100% 100%; +} + +.pytorch-colab { + background-image: url("../images/logo-colab.svg"); +} + +.pytorch-download { + background-image: url("../images/logo-download.svg"); +} + +.pytorch-github { + background-image: url("../images/logo-github.svg"); +} + + + +.call-to-action-desktop-view { + display: none; +} + +.call-to-action-mobile-view { + display: block; +} + +#google-colab-link, #download-notebook-link, #github-view-link { + padding-bottom: 0.625rem; + display: flex; +} + + + +@media screen and (min-width: 768px) { + .call-to-action-desktop-view { + display: block; + } + + .call-to-action-mobile-view { + display: none; + } +} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000000..d06a71d751 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000000..d502447d2d --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.6.4', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: true, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/drawio/dfe/user_actor.png b/_static/drawio/dfe/user_actor.png new file mode 100644 index 0000000000..220270aafb Binary files /dev/null and b/_static/drawio/dfe/user_actor.png differ diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000000..a858a410e4 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/images/additional_stats.png b/_static/images/additional_stats.png new file mode 100644 index 0000000000..2b9d2c3f5e Binary files /dev/null and b/_static/images/additional_stats.png differ diff --git a/_static/images/annotations.png b/_static/images/annotations.png new file mode 100644 index 0000000000..b1e524890a Binary files /dev/null and b/_static/images/annotations.png differ diff --git a/_static/images/benchmark_compare.png b/_static/images/benchmark_compare.png new file mode 100644 index 0000000000..8765ed48c7 Binary files /dev/null and b/_static/images/benchmark_compare.png differ diff --git a/_static/images/benchmark_mass_compare.png b/_static/images/benchmark_mass_compare.png new file mode 100644 index 0000000000..71911c5546 Binary files /dev/null and b/_static/images/benchmark_mass_compare.png differ diff --git a/_static/images/benchmark_sets.png b/_static/images/benchmark_sets.png new file mode 100644 index 0000000000..4eadd28734 Binary files /dev/null and b/_static/images/benchmark_sets.png differ diff --git a/_static/images/benchmark_view.png b/_static/images/benchmark_view.png new file mode 100644 index 0000000000..c8a5afc810 Binary files /dev/null and b/_static/images/benchmark_view.png differ diff --git a/_static/images/databases.png b/_static/images/databases.png new file mode 100644 index 0000000000..167c4ce615 Binary files /dev/null and b/_static/images/databases.png differ diff --git a/_static/images/general_stats.png b/_static/images/general_stats.png new file mode 100644 index 0000000000..660f2ed89b Binary files /dev/null and b/_static/images/general_stats.png differ diff --git a/_static/images/logo-colab.svg b/_static/images/logo-colab.svg new file mode 100644 index 0000000000..2ab15e2f30 --- /dev/null +++ b/_static/images/logo-colab.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/_static/images/logo-deeppavlov.svg b/_static/images/logo-deeppavlov.svg new file mode 100644 index 0000000000..5aa494ffaf --- /dev/null +++ b/_static/images/logo-deeppavlov.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/images/logo-dff.svg b/_static/images/logo-dff.svg new file mode 100644 index 0000000000..b2f644b0c9 --- /dev/null +++ b/_static/images/logo-dff.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/images/logo-download.svg b/_static/images/logo-download.svg new file mode 100644 index 0000000000..cc37d638e9 --- /dev/null +++ b/_static/images/logo-download.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/_static/images/logo-github.svg b/_static/images/logo-github.svg new file mode 100644 index 0000000000..2c2570da1d --- /dev/null +++ b/_static/images/logo-github.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/_static/images/logo-simple.svg b/_static/images/logo-simple.svg new file mode 100644 index 0000000000..b2f644b0c9 --- /dev/null +++ b/_static/images/logo-simple.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/images/overview.png b/_static/images/overview.png new file mode 100644 index 0000000000..5bd0fb6a1b Binary files /dev/null and b/_static/images/overview.png differ diff --git a/_static/images/service_stats.png b/_static/images/service_stats.png new file mode 100644 index 0000000000..7ce7242863 Binary files /dev/null and b/_static/images/service_stats.png differ diff --git a/_static/jupyterlite_badge_logo.svg b/_static/jupyterlite_badge_logo.svg new file mode 100644 index 0000000000..5de36d7fd5 --- /dev/null +++ b/_static/jupyterlite_badge_logo.svg @@ -0,0 +1,3 @@ + + +launchlaunchlitelite \ No newline at end of file diff --git a/_static/katex-math.css b/_static/katex-math.css new file mode 100644 index 0000000000..bdd1634d81 --- /dev/null +++ b/_static/katex-math.css @@ -0,0 +1,50 @@ +/* Responsives: make equations scrollable on small screens. + * See: https://github.com/Khan/KaTeX/issues/327 */ +.katex-display > .katex { + max-width: 100%; +} +.katex-display > .katex > .katex-html { + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 1px; + padding-top: 3px; +} +/* Increase margin around equations */ +.katex-display { + margin: 1.2em 0; +} +/* Equation number floats to the right and shows permalink for mouse hover + on the right side of equation number. */ +div.math { + position: relative; + padding-right: 2.5em; +} +.eqno { + height: 100%; + position: absolute; + right: 0; + padding-left: 5px; + padding-bottom: 5px; + padding-right: 1px; +} +.eqno:before { + /* Force vertical alignment of number */ + display: inline-block; + height: 100%; + vertical-align: middle; + content: ""; +} +.eqno .headerlink { + display: none; + visibility: hidden; + font-size: 14px; + padding-left: .3em; +} +.eqno:hover .headerlink { + display: inline-block; + visibility: visible; + margin-right: -1.05em; +} diff --git a/_static/katex.min.js b/_static/katex.min.js new file mode 100644 index 0000000000..a919bd4407 --- /dev/null +++ b/_static/katex.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.katex=t():e.katex=t()}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return Qn}});var r=function e(t,r){this.position=void 0;var n,a="KaTeX parse error: "+t,i=r&&r.loc;if(i&&i.start<=i.end){var o=i.lexer.input;n=i.start;var s=i.end;n===o.length?a+=" at end of input: ":a+=" at position "+(n+1)+": ";var l=o.slice(n,s).replace(/[^]/g,"$&\u0332");a+=(n>15?"\u2026"+o.slice(n-15,n):o.slice(0,n))+l+(s+15":">","<":"<",'"':""","'":"'"},o=/[&><"']/g;var s=function e(t){return"ordgroup"===t.type||"color"===t.type?1===t.body.length?e(t.body[0]):t:"font"===t.type?e(t.body):t},l={contains:function(e,t){return-1!==e.indexOf(t)},deflt:function(e,t){return void 0===e?t:e},escape:function(e){return String(e).replace(o,(function(e){return i[e]}))},hyphenate:function(e){return e.replace(a,"-$1").toLowerCase()},getBaseElem:s,isCharacterBox:function(e){var t=s(e);return"mathord"===t.type||"textord"===t.type||"atom"===t.type},protocolFromUrl:function(e){var t=/^\s*([^\\/#]*?)(?::|�*58|�*3a)/i.exec(e);return null!=t?t[1]:"_relative"}},h={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format "},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color ",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:function(e){return"#"+e}},macros:{type:"object",cli:"-m, --macro ",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:function(e,t){return t.push(e),t}},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:function(e){return Math.max(0,e)},cli:"--min-rule-thickness ",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:function(e){return Math.max(0,e)},cli:"-s, --max-size ",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:function(e){return Math.max(0,e)},cli:"-e, --max-expand ",cliProcessor:function(e){return"Infinity"===e?1/0:parseInt(e)}},globalGroup:{type:"boolean",cli:!1}};function m(e){if(e.default)return e.default;var t=e.type,r=Array.isArray(t)?t[0]:t;if("string"!=typeof r)return r.enum[0];switch(r){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}var c=function(){function e(e){for(var t in this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,e=e||{},h)if(h.hasOwnProperty(t)){var r=h[t];this[t]=void 0!==e[t]?r.processor?r.processor(e[t]):e[t]:m(r)}}var t=e.prototype;return t.reportNonstrict=function(e,t,r){var a=this.strict;if("function"==typeof a&&(a=a(e,t,r)),a&&"ignore"!==a){if(!0===a||"error"===a)throw new n("LaTeX-incompatible input and strict mode is set to 'error': "+t+" ["+e+"]",r);"warn"===a?"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+t+" ["+e+"]"):"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+a+"': "+t+" ["+e+"]")}},t.useStrictBehavior=function(e,t,r){var n=this.strict;if("function"==typeof n)try{n=n(e,t,r)}catch(e){n="error"}return!(!n||"ignore"===n)&&(!0===n||"error"===n||("warn"===n?("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+t+" ["+e+"]"),!1):("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+n+"': "+t+" ["+e+"]"),!1)))},t.isTrusted=function(e){e.url&&!e.protocol&&(e.protocol=l.protocolFromUrl(e.url));var t="function"==typeof this.trust?this.trust(e):this.trust;return Boolean(t)},e}(),u=function(){function e(e,t,r){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=e,this.size=t,this.cramped=r}var t=e.prototype;return t.sup=function(){return p[d[this.id]]},t.sub=function(){return p[f[this.id]]},t.fracNum=function(){return p[g[this.id]]},t.fracDen=function(){return p[v[this.id]]},t.cramp=function(){return p[b[this.id]]},t.text=function(){return p[y[this.id]]},t.isTight=function(){return this.size>=2},e}(),p=[new u(0,0,!1),new u(1,0,!0),new u(2,1,!1),new u(3,1,!0),new u(4,2,!1),new u(5,2,!0),new u(6,3,!1),new u(7,3,!0)],d=[4,5,4,5,6,7,6,7],f=[5,5,5,5,7,7,7,7],g=[2,3,4,5,6,7,6,7],v=[3,3,5,5,7,7,7,7],b=[1,1,3,3,5,5,7,7],y=[0,1,2,3,2,3,2,3],x={DISPLAY:p[0],TEXT:p[2],SCRIPT:p[4],SCRIPTSCRIPT:p[6]},w=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"armenian",blocks:[[1328,1423]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];var k=[];function S(e){for(var t=0;t=k[t]&&e<=k[t+1])return!0;return!1}w.forEach((function(e){return e.blocks.forEach((function(e){return k.push.apply(k,e)}))}));var M=80,z={doubleleftarrow:"M262 157\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\nm8 0v40h399730v-40zm0 194v40h399730v-40z",doublerightarrow:"M399738 392l\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z",leftarrow:"M400000 241H110l3-3c68.7-52.7 113.7-120\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\n l-3-3h399890zM100 241v40h399900v-40z",leftbrace:"M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z",leftbraceunder:"M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z",leftgroup:"M400000 80\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\n 435 0h399565z",leftgroupunder:"M400000 262\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\n 435 219h399565z",leftharpoon:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z",leftharpoonplus:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\nm0 0v40h400000v-40z",leftharpoondown:"M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z",leftharpoondownplus:"M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z",lefthook:"M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\n 71.5 23h399859zM103 281v-40h399897v40z",leftlinesegment:"M40 281 V428 H0 V94 H40 V241 H400000 v40z\nM40 281 V428 H0 V94 H40 V241 H400000 v40z",leftmapsto:"M40 281 V448H0V74H40V241H400000v40z\nM40 281 V448H0V74H40V241H400000v40z",leftToFrom:"M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z",longequal:"M0 50 h400000 v40H0z m0 194h40000v40H0z\nM0 50 h400000 v40H0z m0 194h40000v40H0z",midbrace:"M200428 334\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z",midbraceunder:"M199572 214\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z",oiintSize1:"M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z",oiintSize2:"M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\nc0 110 84 276 504 276s502.4-166 502.4-276z",oiiintSize1:"M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z",oiiintSize2:"M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z",rightarrow:"M0 241v40h399891c-47.3 35.3-84 78-110 128\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n 151.7 139 205zm0 0v40h399900v-40z",rightbrace:"M400000 542l\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z",rightbraceunder:"M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z",rightgroup:"M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\n 3-1 3-3v-38c-76-158-257-219-435-219H0z",rightgroupunder:"M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z",rightharpoon:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\n 69.2 92 94.5zm0 0v40h399900v-40z",rightharpoonplus:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z",rightharpoondown:"M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z",rightharpoondownplus:"M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\nm0-194v40h400000v-40zm0 0v40h400000v-40z",righthook:"M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z",rightlinesegment:"M399960 241 V94 h40 V428 h-40 V281 H0 v-40z\nM399960 241 V94 h40 V428 h-40 V281 H0 v-40z",rightToFrom:"M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z",twoheadleftarrow:"M0 167c68 40\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z",twoheadrightarrow:"M400000 167\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z",tilde1:"M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\n-68.267.847-113-73.952-191-73.952z",tilde2:"M344 55.266c-142 0-300.638 81.316-311.5 86.418\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z",tilde3:"M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\n -338 0-409-156.573-744-156.573z",tilde4:"M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\n -175.236-744-175.236z",vec:"M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\nc-16-25.333-24-45-24-59z",widehat1:"M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z",widehat2:"M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat3:"M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat4:"M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widecheck1:"M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z",widecheck2:"M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck3:"M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck4:"M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",baraboveleftarrow:"M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z",rightarrowabovebar:"M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z",baraboveshortleftharpoon:"M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z",rightharpoonaboveshortbar:"M0,241 l0,40c399126,0,399993,0,399993,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z",shortbaraboveleftharpoon:"M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z",shortrightharpoonabovebar:"M53,241l0,40c398570,0,399437,0,399437,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z"},A=function(){function e(e){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=e,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}var t=e.prototype;return t.hasClass=function(e){return l.contains(this.classes,e)},t.toNode=function(){for(var e=document.createDocumentFragment(),t=0;t=5?0:e>=3?1:2]){var r=N[t]={cssEmPerMu:B.quad[t]/18};for(var n in B)B.hasOwnProperty(n)&&(r[n]=B[n][t])}return N[t]}(this.size)),this._fontMetrics},t.getColor=function(){return this.phantom?"transparent":this.color},e}();H.BASESIZE=6;var E=H,L={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:1.00375,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:1.00375},D={ex:!0,em:!0,mu:!0},P=function(e){return"string"!=typeof e&&(e=e.unit),e in L||e in D||"ex"===e},F=function(e,t){var r;if(e.unit in L)r=L[e.unit]/t.fontMetrics().ptPerEm/t.sizeMultiplier;else if("mu"===e.unit)r=t.fontMetrics().cssEmPerMu;else{var a;if(a=t.style.isTight()?t.havingStyle(t.style.text()):t,"ex"===e.unit)r=a.fontMetrics().xHeight;else{if("em"!==e.unit)throw new n("Invalid unit: '"+e.unit+"'");r=a.fontMetrics().quad}a!==t&&(r*=a.sizeMultiplier/t.sizeMultiplier)}return Math.min(e.number*r,t.maxSize)},V=function(e){return+e.toFixed(4)+"em"},G=function(e){return e.filter((function(e){return e})).join(" ")},U=function(e,t,r){if(this.classes=e||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=r||{},t){t.style.isTight()&&this.classes.push("mtight");var n=t.getColor();n&&(this.style.color=n)}},Y=function(e){var t=document.createElement(e);for(var r in t.className=G(this.classes),this.style)this.style.hasOwnProperty(r)&&(t.style[r]=this.style[r]);for(var n in this.attributes)this.attributes.hasOwnProperty(n)&&t.setAttribute(n,this.attributes[n]);for(var a=0;a"},W=function(){function e(e,t,r,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,U.call(this,e,r,n),this.children=t||[]}var t=e.prototype;return t.setAttribute=function(e,t){this.attributes[e]=t},t.hasClass=function(e){return l.contains(this.classes,e)},t.toNode=function(){return Y.call(this,"span")},t.toMarkup=function(){return X.call(this,"span")},e}(),_=function(){function e(e,t,r,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,U.call(this,t,n),this.children=r||[],this.setAttribute("href",e)}var t=e.prototype;return t.setAttribute=function(e,t){this.attributes[e]=t},t.hasClass=function(e){return l.contains(this.classes,e)},t.toNode=function(){return Y.call(this,"a")},t.toMarkup=function(){return X.call(this,"a")},e}(),j=function(){function e(e,t,r){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=t,this.src=e,this.classes=["mord"],this.style=r}var t=e.prototype;return t.hasClass=function(e){return l.contains(this.classes,e)},t.toNode=function(){var e=document.createElement("img");for(var t in e.src=this.src,e.alt=this.alt,e.className="mord",this.style)this.style.hasOwnProperty(t)&&(e.style[t]=this.style[t]);return e},t.toMarkup=function(){var e=""+this.alt+"=a[0]&&e<=a[1])return r.name}return null}(this.text.charCodeAt(0));l&&this.classes.push(l+"_fallback"),/[\xee\xef\xed\xec]/.test(this.text)&&(this.text=$[this.text])}var t=e.prototype;return t.hasClass=function(e){return l.contains(this.classes,e)},t.toNode=function(){var e=document.createTextNode(this.text),t=null;for(var r in this.italic>0&&((t=document.createElement("span")).style.marginRight=V(this.italic)),this.classes.length>0&&((t=t||document.createElement("span")).className=G(this.classes)),this.style)this.style.hasOwnProperty(r)&&((t=t||document.createElement("span")).style[r]=this.style[r]);return t?(t.appendChild(e),t):e},t.toMarkup=function(){var e=!1,t="0&&(r+="margin-right:"+this.italic+"em;"),this.style)this.style.hasOwnProperty(n)&&(r+=l.hyphenate(n)+":"+this.style[n]+";");r&&(e=!0,t+=' style="'+l.escape(r)+'"');var a=l.escape(this.text);return e?(t+=">",t+=a,t+=""):a},e}(),K=function(){function e(e,t){this.children=void 0,this.attributes=void 0,this.children=e||[],this.attributes=t||{}}var t=e.prototype;return t.toNode=function(){var e=document.createElementNS("http://www.w3.org/2000/svg","svg");for(var t in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,t)&&e.setAttribute(t,this.attributes[t]);for(var r=0;r":""},e}(),Q=function(){function e(e){this.attributes=void 0,this.attributes=e||{}}var t=e.prototype;return t.toNode=function(){var e=document.createElementNS("http://www.w3.org/2000/svg","line");for(var t in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,t)&&e.setAttribute(t,this.attributes[t]);return e},t.toMarkup=function(){var e="","\\gt",!0),ie(oe,le,be,"\u2208","\\in",!0),ie(oe,le,be,"\ue020","\\@not"),ie(oe,le,be,"\u2282","\\subset",!0),ie(oe,le,be,"\u2283","\\supset",!0),ie(oe,le,be,"\u2286","\\subseteq",!0),ie(oe,le,be,"\u2287","\\supseteq",!0),ie(oe,he,be,"\u2288","\\nsubseteq",!0),ie(oe,he,be,"\u2289","\\nsupseteq",!0),ie(oe,le,be,"\u22a8","\\models"),ie(oe,le,be,"\u2190","\\leftarrow",!0),ie(oe,le,be,"\u2264","\\le"),ie(oe,le,be,"\u2264","\\leq",!0),ie(oe,le,be,"<","\\lt",!0),ie(oe,le,be,"\u2192","\\rightarrow",!0),ie(oe,le,be,"\u2192","\\to"),ie(oe,he,be,"\u2271","\\ngeq",!0),ie(oe,he,be,"\u2270","\\nleq",!0),ie(oe,le,ye,"\xa0","\\ "),ie(oe,le,ye,"\xa0","\\space"),ie(oe,le,ye,"\xa0","\\nobreakspace"),ie(se,le,ye,"\xa0","\\ "),ie(se,le,ye,"\xa0"," "),ie(se,le,ye,"\xa0","\\space"),ie(se,le,ye,"\xa0","\\nobreakspace"),ie(oe,le,ye,null,"\\nobreak"),ie(oe,le,ye,null,"\\allowbreak"),ie(oe,le,ve,",",","),ie(oe,le,ve,";",";"),ie(oe,he,ce,"\u22bc","\\barwedge",!0),ie(oe,he,ce,"\u22bb","\\veebar",!0),ie(oe,le,ce,"\u2299","\\odot",!0),ie(oe,le,ce,"\u2295","\\oplus",!0),ie(oe,le,ce,"\u2297","\\otimes",!0),ie(oe,le,xe,"\u2202","\\partial",!0),ie(oe,le,ce,"\u2298","\\oslash",!0),ie(oe,he,ce,"\u229a","\\circledcirc",!0),ie(oe,he,ce,"\u22a1","\\boxdot",!0),ie(oe,le,ce,"\u25b3","\\bigtriangleup"),ie(oe,le,ce,"\u25bd","\\bigtriangledown"),ie(oe,le,ce,"\u2020","\\dagger"),ie(oe,le,ce,"\u22c4","\\diamond"),ie(oe,le,ce,"\u22c6","\\star"),ie(oe,le,ce,"\u25c3","\\triangleleft"),ie(oe,le,ce,"\u25b9","\\triangleright"),ie(oe,le,ge,"{","\\{"),ie(se,le,xe,"{","\\{"),ie(se,le,xe,"{","\\textbraceleft"),ie(oe,le,ue,"}","\\}"),ie(se,le,xe,"}","\\}"),ie(se,le,xe,"}","\\textbraceright"),ie(oe,le,ge,"{","\\lbrace"),ie(oe,le,ue,"}","\\rbrace"),ie(oe,le,ge,"[","\\lbrack",!0),ie(se,le,xe,"[","\\lbrack",!0),ie(oe,le,ue,"]","\\rbrack",!0),ie(se,le,xe,"]","\\rbrack",!0),ie(oe,le,ge,"(","\\lparen",!0),ie(oe,le,ue,")","\\rparen",!0),ie(se,le,xe,"<","\\textless",!0),ie(se,le,xe,">","\\textgreater",!0),ie(oe,le,ge,"\u230a","\\lfloor",!0),ie(oe,le,ue,"\u230b","\\rfloor",!0),ie(oe,le,ge,"\u2308","\\lceil",!0),ie(oe,le,ue,"\u2309","\\rceil",!0),ie(oe,le,xe,"\\","\\backslash"),ie(oe,le,xe,"\u2223","|"),ie(oe,le,xe,"\u2223","\\vert"),ie(se,le,xe,"|","\\textbar",!0),ie(oe,le,xe,"\u2225","\\|"),ie(oe,le,xe,"\u2225","\\Vert"),ie(se,le,xe,"\u2225","\\textbardbl"),ie(se,le,xe,"~","\\textasciitilde"),ie(se,le,xe,"\\","\\textbackslash"),ie(se,le,xe,"^","\\textasciicircum"),ie(oe,le,be,"\u2191","\\uparrow",!0),ie(oe,le,be,"\u21d1","\\Uparrow",!0),ie(oe,le,be,"\u2193","\\downarrow",!0),ie(oe,le,be,"\u21d3","\\Downarrow",!0),ie(oe,le,be,"\u2195","\\updownarrow",!0),ie(oe,le,be,"\u21d5","\\Updownarrow",!0),ie(oe,le,fe,"\u2210","\\coprod"),ie(oe,le,fe,"\u22c1","\\bigvee"),ie(oe,le,fe,"\u22c0","\\bigwedge"),ie(oe,le,fe,"\u2a04","\\biguplus"),ie(oe,le,fe,"\u22c2","\\bigcap"),ie(oe,le,fe,"\u22c3","\\bigcup"),ie(oe,le,fe,"\u222b","\\int"),ie(oe,le,fe,"\u222b","\\intop"),ie(oe,le,fe,"\u222c","\\iint"),ie(oe,le,fe,"\u222d","\\iiint"),ie(oe,le,fe,"\u220f","\\prod"),ie(oe,le,fe,"\u2211","\\sum"),ie(oe,le,fe,"\u2a02","\\bigotimes"),ie(oe,le,fe,"\u2a01","\\bigoplus"),ie(oe,le,fe,"\u2a00","\\bigodot"),ie(oe,le,fe,"\u222e","\\oint"),ie(oe,le,fe,"\u222f","\\oiint"),ie(oe,le,fe,"\u2230","\\oiiint"),ie(oe,le,fe,"\u2a06","\\bigsqcup"),ie(oe,le,fe,"\u222b","\\smallint"),ie(se,le,pe,"\u2026","\\textellipsis"),ie(oe,le,pe,"\u2026","\\mathellipsis"),ie(se,le,pe,"\u2026","\\ldots",!0),ie(oe,le,pe,"\u2026","\\ldots",!0),ie(oe,le,pe,"\u22ef","\\@cdots",!0),ie(oe,le,pe,"\u22f1","\\ddots",!0),ie(oe,le,xe,"\u22ee","\\varvdots"),ie(oe,le,me,"\u02ca","\\acute"),ie(oe,le,me,"\u02cb","\\grave"),ie(oe,le,me,"\xa8","\\ddot"),ie(oe,le,me,"~","\\tilde"),ie(oe,le,me,"\u02c9","\\bar"),ie(oe,le,me,"\u02d8","\\breve"),ie(oe,le,me,"\u02c7","\\check"),ie(oe,le,me,"^","\\hat"),ie(oe,le,me,"\u20d7","\\vec"),ie(oe,le,me,"\u02d9","\\dot"),ie(oe,le,me,"\u02da","\\mathring"),ie(oe,le,de,"\ue131","\\@imath"),ie(oe,le,de,"\ue237","\\@jmath"),ie(oe,le,xe,"\u0131","\u0131"),ie(oe,le,xe,"\u0237","\u0237"),ie(se,le,xe,"\u0131","\\i",!0),ie(se,le,xe,"\u0237","\\j",!0),ie(se,le,xe,"\xdf","\\ss",!0),ie(se,le,xe,"\xe6","\\ae",!0),ie(se,le,xe,"\u0153","\\oe",!0),ie(se,le,xe,"\xf8","\\o",!0),ie(se,le,xe,"\xc6","\\AE",!0),ie(se,le,xe,"\u0152","\\OE",!0),ie(se,le,xe,"\xd8","\\O",!0),ie(se,le,me,"\u02ca","\\'"),ie(se,le,me,"\u02cb","\\`"),ie(se,le,me,"\u02c6","\\^"),ie(se,le,me,"\u02dc","\\~"),ie(se,le,me,"\u02c9","\\="),ie(se,le,me,"\u02d8","\\u"),ie(se,le,me,"\u02d9","\\."),ie(se,le,me,"\xb8","\\c"),ie(se,le,me,"\u02da","\\r"),ie(se,le,me,"\u02c7","\\v"),ie(se,le,me,"\xa8",'\\"'),ie(se,le,me,"\u02dd","\\H"),ie(se,le,me,"\u25ef","\\textcircled");var we={"--":!0,"---":!0,"``":!0,"''":!0};ie(se,le,xe,"\u2013","--",!0),ie(se,le,xe,"\u2013","\\textendash"),ie(se,le,xe,"\u2014","---",!0),ie(se,le,xe,"\u2014","\\textemdash"),ie(se,le,xe,"\u2018","`",!0),ie(se,le,xe,"\u2018","\\textquoteleft"),ie(se,le,xe,"\u2019","'",!0),ie(se,le,xe,"\u2019","\\textquoteright"),ie(se,le,xe,"\u201c","``",!0),ie(se,le,xe,"\u201c","\\textquotedblleft"),ie(se,le,xe,"\u201d","''",!0),ie(se,le,xe,"\u201d","\\textquotedblright"),ie(oe,le,xe,"\xb0","\\degree",!0),ie(se,le,xe,"\xb0","\\degree"),ie(se,le,xe,"\xb0","\\textdegree",!0),ie(oe,le,xe,"\xa3","\\pounds"),ie(oe,le,xe,"\xa3","\\mathsterling",!0),ie(se,le,xe,"\xa3","\\pounds"),ie(se,le,xe,"\xa3","\\textsterling",!0),ie(oe,he,xe,"\u2720","\\maltese"),ie(se,he,xe,"\u2720","\\maltese");for(var ke='0123456789/@."',Se=0;Set&&(t=i.height),i.depth>r&&(r=i.depth),i.maxFontSize>n&&(n=i.maxFontSize)}e.height=t,e.depth=r,e.maxFontSize=n},Xe=function(e,t,r,n){var a=new W(e,t,r,n);return Ye(a),a},We=function(e,t,r,n){return new W(e,t,r,n)},_e=function(e){var t=new A(e);return Ye(t),t},je=function(e,t,r){var n="";switch(e){case"amsrm":n="AMS";break;case"textrm":n="Main";break;case"textsf":n="SansSerif";break;case"texttt":n="Typewriter";break;default:n=e}return n+"-"+("textbf"===t&&"textit"===r?"BoldItalic":"textbf"===t?"Bold":"textit"===t?"Italic":"Regular")},$e={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},Ze={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},Ke={fontMap:$e,makeSymbol:Ge,mathsym:function(e,t,r,n){return void 0===n&&(n=[]),"boldsymbol"===r.font&&Ve(e,"Main-Bold",t).metrics?Ge(e,"Main-Bold",t,r,n.concat(["mathbf"])):"\\"===e||"main"===ae[t][e].font?Ge(e,"Main-Regular",t,r,n):Ge(e,"AMS-Regular",t,r,n.concat(["amsrm"]))},makeSpan:Xe,makeSvgSpan:We,makeLineSpan:function(e,t,r){var n=Xe([e],[],t);return n.height=Math.max(r||t.fontMetrics().defaultRuleThickness,t.minRuleThickness),n.style.borderBottomWidth=V(n.height),n.maxFontSize=1,n},makeAnchor:function(e,t,r,n){var a=new _(e,t,r,n);return Ye(a),a},makeFragment:_e,wrapFragment:function(e,t){return e instanceof A?Xe([],[e],t):e},makeVList:function(e,t){for(var r=function(e){if("individualShift"===e.positionType){for(var t=e.children,r=[t[0]],n=-t[0].shift-t[0].elem.depth,a=n,i=1;i0&&(o.push(kt(s,t)),s=[]),o.push(a[l]));s.length>0&&o.push(kt(s,t)),r?((i=kt(ft(r,t,!0))).classes=["tag"],o.push(i)):n&&o.push(n);var m=mt(["katex-html"],o);if(m.setAttribute("aria-hidden","true"),i){var c=i.children[0];c.style.height=V(m.height+m.depth),m.depth&&(c.style.verticalAlign=V(-m.depth))}return m}function Mt(e){return new A(e)}var zt=function(){function e(e,t,r){this.type=void 0,this.attributes=void 0,this.children=void 0,this.classes=void 0,this.type=e,this.attributes={},this.children=t||[],this.classes=r||[]}var t=e.prototype;return t.setAttribute=function(e,t){this.attributes[e]=t},t.getAttribute=function(e){return this.attributes[e]},t.toNode=function(){var e=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var t in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,t)&&e.setAttribute(t,this.attributes[t]);this.classes.length>0&&(e.className=G(this.classes));for(var r=0;r0&&(e+=' class ="'+l.escape(G(this.classes))+'"'),e+=">";for(var r=0;r"},t.toText=function(){return this.children.map((function(e){return e.toText()})).join("")},e}(),At=function(){function e(e){this.text=void 0,this.text=e}var t=e.prototype;return t.toNode=function(){return document.createTextNode(this.text)},t.toMarkup=function(){return l.escape(this.toText())},t.toText=function(){return this.text},e}(),Tt={MathNode:zt,TextNode:At,SpaceNode:function(){function e(e){this.width=void 0,this.character=void 0,this.width=e,this.character=e>=.05555&&e<=.05556?"\u200a":e>=.1666&&e<=.1667?"\u2009":e>=.2222&&e<=.2223?"\u2005":e>=.2777&&e<=.2778?"\u2005\u200a":e>=-.05556&&e<=-.05555?"\u200a\u2063":e>=-.1667&&e<=-.1666?"\u2009\u2063":e>=-.2223&&e<=-.2222?"\u205f\u2063":e>=-.2778&&e<=-.2777?"\u2005\u2063":null}var t=e.prototype;return t.toNode=function(){if(this.character)return document.createTextNode(this.character);var e=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return e.setAttribute("width",V(this.width)),e},t.toMarkup=function(){return this.character?""+this.character+"":''},t.toText=function(){return this.character?this.character:" "},e}(),newDocumentFragment:Mt},Bt=function(e,t,r){return!ae[t][e]||!ae[t][e].replace||55349===e.charCodeAt(0)||we.hasOwnProperty(e)&&r&&(r.fontFamily&&"tt"===r.fontFamily.substr(4,2)||r.font&&"tt"===r.font.substr(4,2))||(e=ae[t][e].replace),new Tt.TextNode(e)},Ct=function(e){return 1===e.length?e[0]:new Tt.MathNode("mrow",e)},qt=function(e,t){if("texttt"===t.fontFamily)return"monospace";if("textsf"===t.fontFamily)return"textit"===t.fontShape&&"textbf"===t.fontWeight?"sans-serif-bold-italic":"textit"===t.fontShape?"sans-serif-italic":"textbf"===t.fontWeight?"bold-sans-serif":"sans-serif";if("textit"===t.fontShape&&"textbf"===t.fontWeight)return"bold-italic";if("textit"===t.fontShape)return"italic";if("textbf"===t.fontWeight)return"bold";var r=t.font;if(!r||"mathnormal"===r)return null;var n=e.mode;if("mathit"===r)return"italic";if("boldsymbol"===r)return"textord"===e.type?"bold":"bold-italic";if("mathbf"===r)return"bold";if("mathbb"===r)return"double-struck";if("mathfrak"===r)return"fraktur";if("mathscr"===r||"mathcal"===r)return"script";if("mathsf"===r)return"sans-serif";if("mathtt"===r)return"monospace";var a=e.text;return l.contains(["\\imath","\\jmath"],a)?null:(ae[n][a]&&ae[n][a].replace&&(a=ae[n][a].replace),q(a,Ke.fontMap[r].fontName,n)?Ke.fontMap[r].variant:null)},Nt=function(e,t,r){if(1===e.length){var n=Rt(e[0],t);return r&&n instanceof zt&&"mo"===n.type&&(n.setAttribute("lspace","0em"),n.setAttribute("rspace","0em")),[n]}for(var a,i=[],o=0;o0&&(p.text=p.text.slice(0,1)+"\u0338"+p.text.slice(1),i.pop())}}}i.push(s),a=s}return i},It=function(e,t,r){return Ct(Nt(e,t,r))},Rt=function(e,t){if(!e)return new Tt.MathNode("mrow");if(it[e.type])return it[e.type](e,t);throw new n("Got group of unknown type: '"+e.type+"'")};function Ot(e,t,r,n,a){var i,o=Nt(e,r);i=1===o.length&&o[0]instanceof zt&&l.contains(["mrow","mtable"],o[0].type)?o[0]:new Tt.MathNode("mrow",o);var s=new Tt.MathNode("annotation",[new Tt.TextNode(t)]);s.setAttribute("encoding","application/x-tex");var h=new Tt.MathNode("semantics",[i,s]),m=new Tt.MathNode("math",[h]);m.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),n&&m.setAttribute("display","block");var c=a?"katex":"katex-mathml";return Ke.makeSpan([c],[m])}var Ht=function(e){return new E({style:e.displayMode?x.DISPLAY:x.TEXT,maxSize:e.maxSize,minRuleThickness:e.minRuleThickness})},Et=function(e,t){if(t.displayMode){var r=["katex-display"];t.leqno&&r.push("leqno"),t.fleqn&&r.push("fleqn"),e=Ke.makeSpan(r,[e])}return e},Lt=function(e,t,r){var n,a=Ht(r);if("mathml"===r.output)return Ot(e,t,a,r.displayMode,!0);if("html"===r.output){var i=St(e,a);n=Ke.makeSpan(["katex"],[i])}else{var o=Ot(e,t,a,r.displayMode,!1),s=St(e,a);n=Ke.makeSpan(["katex"],[o,s])}return Et(n,r)},Dt={widehat:"^",widecheck:"\u02c7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23df",overbrace:"\u23de",overgroup:"\u23e0",undergroup:"\u23e1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21d2",xRightarrow:"\u21d2",overleftharpoon:"\u21bc",xleftharpoonup:"\u21bc",overrightharpoon:"\u21c0",xrightharpoonup:"\u21c0",xLeftarrow:"\u21d0",xLeftrightarrow:"\u21d4",xhookleftarrow:"\u21a9",xhookrightarrow:"\u21aa",xmapsto:"\u21a6",xrightharpoondown:"\u21c1",xleftharpoondown:"\u21bd",xrightleftharpoons:"\u21cc",xleftrightharpoons:"\u21cb",xtwoheadleftarrow:"\u219e",xtwoheadrightarrow:"\u21a0",xlongequal:"=",xtofrom:"\u21c4",xrightleftarrows:"\u21c4",xrightequilibrium:"\u21cc",xleftequilibrium:"\u21cb","\\cdrightarrow":"\u2192","\\cdleftarrow":"\u2190","\\cdlongequal":"="},Pt={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],"\\cdrightarrow":[["rightarrow"],3,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],"\\cdleftarrow":[["leftarrow"],3,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],"\\cdlongequal":[["longequal"],3,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},Ft=function(e,t,r,n,a){var i,o=e.height+e.depth+r+n;if(/fbox|color|angl/.test(t)){if(i=Ke.makeSpan(["stretchy",t],[],a),"fbox"===t){var s=a.color&&a.getColor();s&&(i.style.borderColor=s)}}else{var l=[];/^[bx]cancel$/.test(t)&&l.push(new Q({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(t)&&l.push(new Q({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var h=new K(l,{width:"100%",height:V(o)});i=Ke.makeSvgSpan([],[h],a)}return i.height=o,i.style.height=V(o),i},Vt=function(e){var t=new Tt.MathNode("mo",[new Tt.TextNode(Dt[e.replace(/^\\/,"")])]);return t.setAttribute("stretchy","true"),t},Gt=function(e,t){var r=function(){var r=4e5,n=e.label.substr(1);if(l.contains(["widehat","widecheck","widetilde","utilde"],n)){var a,i,o,s="ordgroup"===(d=e.base).type?d.body.length:1;if(s>5)"widehat"===n||"widecheck"===n?(a=420,r=2364,o=.42,i=n+"4"):(a=312,r=2340,o=.34,i="tilde4");else{var h=[1,1,2,2,3,3][s];"widehat"===n||"widecheck"===n?(r=[0,1062,2364,2364,2364][h],a=[0,239,300,360,420][h],o=[0,.24,.3,.3,.36,.42][h],i=n+h):(r=[0,600,1033,2339,2340][h],a=[0,260,286,306,312][h],o=[0,.26,.286,.3,.306,.34][h],i="tilde"+h)}var m=new J(i),c=new K([m],{width:"100%",height:V(o),viewBox:"0 0 "+r+" "+a,preserveAspectRatio:"none"});return{span:Ke.makeSvgSpan([],[c],t),minWidth:0,height:o}}var u,p,d,f=[],g=Pt[n],v=g[0],b=g[1],y=g[2],x=y/1e3,w=v.length;if(1===w)u=["hide-tail"],p=[g[3]];else if(2===w)u=["halfarrow-left","halfarrow-right"],p=["xMinYMin","xMaxYMin"];else{if(3!==w)throw new Error("Correct katexImagesData or update code here to support\n "+w+" children.");u=["brace-left","brace-center","brace-right"],p=["xMinYMin","xMidYMin","xMaxYMin"]}for(var k=0;k0&&(n.style.minWidth=V(a)),n};function Ut(e,t){if(!e||e.type!==t)throw new Error("Expected node of type "+t+", but got "+(e?"node of type "+e.type:String(e)));return e}function Yt(e){var t=Xt(e);if(!t)throw new Error("Expected node of symbol group type, but got "+(e?"node of type "+e.type:String(e)));return t}function Xt(e){return e&&("atom"===e.type||re.hasOwnProperty(e.type))?e:null}var Wt=function(e,t){var r,n,a;e&&"supsub"===e.type?(r=(n=Ut(e.base,"accent")).base,e.base=r,a=function(e){if(e instanceof W)return e;throw new Error("Expected span but got "+String(e)+".")}(wt(e,t)),e.base=n):r=(n=Ut(e,"accent")).base;var i=wt(r,t.havingCrampedStyle()),o=0;if(n.isShifty&&l.isCharacterBox(r)){var s=l.getBaseElem(r);o=ee(wt(s,t.havingCrampedStyle())).skew}var h,m="\\c"===n.label,c=m?i.height+i.depth:Math.min(i.height,t.fontMetrics().xHeight);if(n.isStretchy)h=Gt(n,t),h=Ke.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:i},{type:"elem",elem:h,wrapperClasses:["svg-align"],wrapperStyle:o>0?{width:"calc(100% - "+V(2*o)+")",marginLeft:V(2*o)}:void 0}]},t);else{var u,p;"\\vec"===n.label?(u=Ke.staticSvg("vec",t),p=Ke.svgData.vec[1]):((u=ee(u=Ke.makeOrd({mode:n.mode,text:n.label},t,"textord"))).italic=0,p=u.width,m&&(c+=u.depth)),h=Ke.makeSpan(["accent-body"],[u]);var d="\\textcircled"===n.label;d&&(h.classes.push("accent-full"),c=i.height);var f=o;d||(f-=p/2),h.style.left=V(f),"\\textcircled"===n.label&&(h.style.top=".2em"),h=Ke.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:i},{type:"kern",size:-c},{type:"elem",elem:h}]},t)}var g=Ke.makeSpan(["mord","accent"],[h],t);return a?(a.children[0]=g,a.height=Math.max(g.height,a.height),a.classes[0]="mord",a):g},_t=function(e,t){var r=e.isStretchy?Vt(e.label):new Tt.MathNode("mo",[Bt(e.label,e.mode)]),n=new Tt.MathNode("mover",[Rt(e.base,t),r]);return n.setAttribute("accent","true"),n},jt=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map((function(e){return"\\"+e})).join("|"));ot({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:function(e,t){var r=lt(t[0]),n=!jt.test(e.funcName),a=!n||"\\widehat"===e.funcName||"\\widetilde"===e.funcName||"\\widecheck"===e.funcName;return{type:"accent",mode:e.parser.mode,label:e.funcName,isStretchy:n,isShifty:a,base:r}},htmlBuilder:Wt,mathmlBuilder:_t}),ot({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\c","\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["primitive"]},handler:function(e,t){var r=t[0],n=e.parser.mode;return"math"===n&&(e.parser.settings.reportNonstrict("mathVsTextAccents","LaTeX's accent "+e.funcName+" works only in text mode"),n="text"),{type:"accent",mode:n,label:e.funcName,isStretchy:!1,isShifty:!0,base:r}},htmlBuilder:Wt,mathmlBuilder:_t}),ot({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"accentUnder",mode:r.mode,label:n,base:a}},htmlBuilder:function(e,t){var r=wt(e.base,t),n=Gt(e,t),a="\\utilde"===e.label?.12:0,i=Ke.makeVList({positionType:"top",positionData:r.height,children:[{type:"elem",elem:n,wrapperClasses:["svg-align"]},{type:"kern",size:a},{type:"elem",elem:r}]},t);return Ke.makeSpan(["mord","accentunder"],[i],t)},mathmlBuilder:function(e,t){var r=Vt(e.label),n=new Tt.MathNode("munder",[Rt(e.base,t),r]);return n.setAttribute("accentunder","true"),n}});var $t=function(e){var t=new Tt.MathNode("mpadded",e?[e]:[]);return t.setAttribute("width","+0.6em"),t.setAttribute("lspace","0.3em"),t};ot({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium","\\\\cdrightarrow","\\\\cdleftarrow","\\\\cdlongequal"],props:{numArgs:1,numOptionalArgs:1},handler:function(e,t,r){var n=e.parser,a=e.funcName;return{type:"xArrow",mode:n.mode,label:a,body:t[0],below:r[0]}},htmlBuilder:function(e,t){var r,n=t.style,a=t.havingStyle(n.sup()),i=Ke.wrapFragment(wt(e.body,a,t),t),o="\\x"===e.label.slice(0,2)?"x":"cd";i.classes.push(o+"-arrow-pad"),e.below&&(a=t.havingStyle(n.sub()),(r=Ke.wrapFragment(wt(e.below,a,t),t)).classes.push(o+"-arrow-pad"));var s,l=Gt(e,t),h=-t.fontMetrics().axisHeight+.5*l.height,m=-t.fontMetrics().axisHeight-.5*l.height-.111;if((i.depth>.25||"\\xleftequilibrium"===e.label)&&(m-=i.depth),r){var c=-t.fontMetrics().axisHeight+r.height+.5*l.height+.111;s=Ke.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:m},{type:"elem",elem:l,shift:h},{type:"elem",elem:r,shift:c}]},t)}else s=Ke.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:m},{type:"elem",elem:l,shift:h}]},t);return s.children[0].children[0].children[1].classes.push("svg-align"),Ke.makeSpan(["mrel","x-arrow"],[s],t)},mathmlBuilder:function(e,t){var r,n=Vt(e.label);if(n.setAttribute("minsize","x"===e.label.charAt(0)?"1.75em":"3.0em"),e.body){var a=$t(Rt(e.body,t));if(e.below){var i=$t(Rt(e.below,t));r=new Tt.MathNode("munderover",[n,i,a])}else r=new Tt.MathNode("mover",[n,a])}else if(e.below){var o=$t(Rt(e.below,t));r=new Tt.MathNode("munder",[n,o])}else r=$t(),r=new Tt.MathNode("mover",[n,r]);return r}});var Zt={">":"\\\\cdrightarrow","<":"\\\\cdleftarrow","=":"\\\\cdlongequal",A:"\\uparrow",V:"\\downarrow","|":"\\Vert",".":"no arrow"},Kt=function(e){return"textord"===e.type&&"@"===e.text};function Jt(e,t,r){var n=Zt[e];switch(n){case"\\\\cdrightarrow":case"\\\\cdleftarrow":return r.callFunction(n,[t[0]],[t[1]]);case"\\uparrow":case"\\downarrow":var a={type:"atom",text:n,mode:"math",family:"rel"},i={type:"ordgroup",mode:"math",body:[r.callFunction("\\\\cdleft",[t[0]],[]),r.callFunction("\\Big",[a],[]),r.callFunction("\\\\cdright",[t[1]],[])]};return r.callFunction("\\\\cdparent",[i],[]);case"\\\\cdlongequal":return r.callFunction("\\\\cdlongequal",[],[]);case"\\Vert":return r.callFunction("\\Big",[{type:"textord",text:"\\Vert",mode:"math"}],[]);default:return{type:"textord",text:" ",mode:"math"}}}ot({type:"cdlabel",names:["\\\\cdleft","\\\\cdright"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName;return{type:"cdlabel",mode:r.mode,side:n.slice(4),label:t[0]}},htmlBuilder:function(e,t){var r=t.havingStyle(t.style.sup()),n=Ke.wrapFragment(wt(e.label,r,t),t);return n.classes.push("cd-label-"+e.side),n.style.bottom=V(.8-n.depth),n.height=0,n.depth=0,n},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mrow",[Rt(e.label,t)]);return(r=new Tt.MathNode("mpadded",[r])).setAttribute("width","0"),"left"===e.side&&r.setAttribute("lspace","-1width"),r.setAttribute("voffset","0.7em"),(r=new Tt.MathNode("mstyle",[r])).setAttribute("displaystyle","false"),r.setAttribute("scriptlevel","1"),r}}),ot({type:"cdlabelparent",names:["\\\\cdparent"],props:{numArgs:1},handler:function(e,t){return{type:"cdlabelparent",mode:e.parser.mode,fragment:t[0]}},htmlBuilder:function(e,t){var r=Ke.wrapFragment(wt(e.fragment,t),t);return r.classes.push("cd-vert-arrow"),r},mathmlBuilder:function(e,t){return new Tt.MathNode("mrow",[Rt(e.fragment,t)])}}),ot({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){for(var r=e.parser,a=Ut(t[0],"ordgroup").body,i="",o=0;o=1114111)throw new n("\\@char with invalid code point "+i);return l<=65535?s=String.fromCharCode(l):(l-=65536,s=String.fromCharCode(55296+(l>>10),56320+(1023&l))),{type:"textord",mode:r.mode,text:s}}});var Qt=function(e,t){var r=ft(e.body,t.withColor(e.color),!1);return Ke.makeFragment(r)},er=function(e,t){var r=Nt(e.body,t.withColor(e.color)),n=new Tt.MathNode("mstyle",r);return n.setAttribute("mathcolor",e.color),n};ot({type:"color",names:["\\textcolor"],props:{numArgs:2,allowedInText:!0,argTypes:["color","original"]},handler:function(e,t){var r=e.parser,n=Ut(t[0],"color-token").color,a=t[1];return{type:"color",mode:r.mode,color:n,body:ht(a)}},htmlBuilder:Qt,mathmlBuilder:er}),ot({type:"color",names:["\\color"],props:{numArgs:1,allowedInText:!0,argTypes:["color"]},handler:function(e,t){var r=e.parser,n=e.breakOnTokenText,a=Ut(t[0],"color-token").color;r.gullet.macros.set("\\current@color",a);var i=r.parseExpression(!0,n);return{type:"color",mode:r.mode,color:a,body:i}},htmlBuilder:Qt,mathmlBuilder:er}),ot({type:"cr",names:["\\\\"],props:{numArgs:0,numOptionalArgs:1,argTypes:["size"],allowedInText:!0},handler:function(e,t,r){var n=e.parser,a=r[0],i=!n.settings.displayMode||!n.settings.useStrictBehavior("newLineInDisplayMode","In LaTeX, \\\\ or \\newline does nothing in display mode");return{type:"cr",mode:n.mode,newLine:i,size:a&&Ut(a,"size").value}},htmlBuilder:function(e,t){var r=Ke.makeSpan(["mspace"],[],t);return e.newLine&&(r.classes.push("newline"),e.size&&(r.style.marginTop=V(F(e.size,t)))),r},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mspace");return e.newLine&&(r.setAttribute("linebreak","newline"),e.size&&r.setAttribute("height",V(F(e.size,t)))),r}});var tr={"\\global":"\\global","\\long":"\\\\globallong","\\\\globallong":"\\\\globallong","\\def":"\\gdef","\\gdef":"\\gdef","\\edef":"\\xdef","\\xdef":"\\xdef","\\let":"\\\\globallet","\\futurelet":"\\\\globalfuture"},rr=function(e){var t=e.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(t))throw new n("Expected a control sequence",e);return t},nr=function(e,t,r,n){var a=e.gullet.macros.get(r.text);null==a&&(r.noexpand=!0,a={tokens:[r],numArgs:0,unexpandable:!e.gullet.isExpandable(r.text)}),e.gullet.macros.set(t,a,n)};ot({type:"internal",names:["\\global","\\long","\\\\globallong"],props:{numArgs:0,allowedInText:!0},handler:function(e){var t=e.parser,r=e.funcName;t.consumeSpaces();var a=t.fetch();if(tr[a.text])return"\\global"!==r&&"\\\\globallong"!==r||(a.text=tr[a.text]),Ut(t.parseFunction(),"internal");throw new n("Invalid token after macro prefix",a)}}),ot({type:"internal",names:["\\def","\\gdef","\\edef","\\xdef"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e){var t=e.parser,r=e.funcName,a=t.gullet.popToken(),i=a.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(i))throw new n("Expected a control sequence",a);for(var o,s=0,l=[[]];"{"!==t.gullet.future().text;)if("#"===(a=t.gullet.popToken()).text){if("{"===t.gullet.future().text){o=t.gullet.future(),l[s].push("{");break}if(a=t.gullet.popToken(),!/^[1-9]$/.test(a.text))throw new n('Invalid argument number "'+a.text+'"');if(parseInt(a.text)!==s+1)throw new n('Argument number "'+a.text+'" out of order');s++,l.push([])}else{if("EOF"===a.text)throw new n("Expected a macro definition");l[s].push(a.text)}var h=t.gullet.consumeArg().tokens;return o&&h.unshift(o),"\\edef"!==r&&"\\xdef"!==r||(h=t.gullet.expandTokens(h)).reverse(),t.gullet.macros.set(i,{tokens:h,numArgs:s,delimiters:l},r===tr[r]),{type:"internal",mode:t.mode}}}),ot({type:"internal",names:["\\let","\\\\globallet"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e){var t=e.parser,r=e.funcName,n=rr(t.gullet.popToken());t.gullet.consumeSpaces();var a=function(e){var t=e.gullet.popToken();return"="===t.text&&" "===(t=e.gullet.popToken()).text&&(t=e.gullet.popToken()),t}(t);return nr(t,n,a,"\\\\globallet"===r),{type:"internal",mode:t.mode}}}),ot({type:"internal",names:["\\futurelet","\\\\globalfuture"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e){var t=e.parser,r=e.funcName,n=rr(t.gullet.popToken()),a=t.gullet.popToken(),i=t.gullet.popToken();return nr(t,n,i,"\\\\globalfuture"===r),t.gullet.pushToken(i),t.gullet.pushToken(a),{type:"internal",mode:t.mode}}});var ar=function(e,t,r){var n=q(ae.math[e]&&ae.math[e].replace||e,t,r);if(!n)throw new Error("Unsupported symbol "+e+" and font size "+t+".");return n},ir=function(e,t,r,n){var a=r.havingBaseStyle(t),i=Ke.makeSpan(n.concat(a.sizingClasses(r)),[e],r),o=a.sizeMultiplier/r.sizeMultiplier;return i.height*=o,i.depth*=o,i.maxFontSize=a.sizeMultiplier,i},or=function(e,t,r){var n=t.havingBaseStyle(r),a=(1-t.sizeMultiplier/n.sizeMultiplier)*t.fontMetrics().axisHeight;e.classes.push("delimcenter"),e.style.top=V(a),e.height-=a,e.depth+=a},sr=function(e,t,r,n,a,i){var o=function(e,t,r,n){return Ke.makeSymbol(e,"Size"+t+"-Regular",r,n)}(e,t,a,n),s=ir(Ke.makeSpan(["delimsizing","size"+t],[o],n),x.TEXT,n,i);return r&&or(s,n,x.TEXT),s},lr=function(e,t,r){var n;return n="Size1-Regular"===t?"delim-size1":"delim-size4",{type:"elem",elem:Ke.makeSpan(["delimsizinginner",n],[Ke.makeSpan([],[Ke.makeSymbol(e,t,r)])])}},hr=function(e,t,r){var n=T["Size4-Regular"][e.charCodeAt(0)]?T["Size4-Regular"][e.charCodeAt(0)][4]:T["Size1-Regular"][e.charCodeAt(0)][4],a=new J("inner",function(e,t){switch(e){case"\u239c":return"M291 0 H417 V"+t+" H291z M291 0 H417 V"+t+" H291z";case"\u2223":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145z";case"\u2225":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145zM367 0 H410 V"+t+" H367z M367 0 H410 V"+t+" H367z";case"\u239f":return"M457 0 H583 V"+t+" H457z M457 0 H583 V"+t+" H457z";case"\u23a2":return"M319 0 H403 V"+t+" H319z M319 0 H403 V"+t+" H319z";case"\u23a5":return"M263 0 H347 V"+t+" H263z M263 0 H347 V"+t+" H263z";case"\u23aa":return"M384 0 H504 V"+t+" H384z M384 0 H504 V"+t+" H384z";case"\u23d0":return"M312 0 H355 V"+t+" H312z M312 0 H355 V"+t+" H312z";case"\u2016":return"M257 0 H300 V"+t+" H257z M257 0 H300 V"+t+" H257zM478 0 H521 V"+t+" H478z M478 0 H521 V"+t+" H478z";default:return""}}(e,Math.round(1e3*t))),i=new K([a],{width:V(n),height:V(t),style:"width:"+V(n),viewBox:"0 0 "+1e3*n+" "+Math.round(1e3*t),preserveAspectRatio:"xMinYMin"}),o=Ke.makeSvgSpan([],[i],r);return o.height=t,o.style.height=V(t),o.style.width=V(n),{type:"elem",elem:o}},mr={type:"kern",size:-.008},cr=["|","\\lvert","\\rvert","\\vert"],ur=["\\|","\\lVert","\\rVert","\\Vert"],pr=function(e,t,r,n,a,i){var o,s,h,m;o=h=m=e,s=null;var c="Size1-Regular";"\\uparrow"===e?h=m="\u23d0":"\\Uparrow"===e?h=m="\u2016":"\\downarrow"===e?o=h="\u23d0":"\\Downarrow"===e?o=h="\u2016":"\\updownarrow"===e?(o="\\uparrow",h="\u23d0",m="\\downarrow"):"\\Updownarrow"===e?(o="\\Uparrow",h="\u2016",m="\\Downarrow"):l.contains(cr,e)?h="\u2223":l.contains(ur,e)?h="\u2225":"["===e||"\\lbrack"===e?(o="\u23a1",h="\u23a2",m="\u23a3",c="Size4-Regular"):"]"===e||"\\rbrack"===e?(o="\u23a4",h="\u23a5",m="\u23a6",c="Size4-Regular"):"\\lfloor"===e||"\u230a"===e?(h=o="\u23a2",m="\u23a3",c="Size4-Regular"):"\\lceil"===e||"\u2308"===e?(o="\u23a1",h=m="\u23a2",c="Size4-Regular"):"\\rfloor"===e||"\u230b"===e?(h=o="\u23a5",m="\u23a6",c="Size4-Regular"):"\\rceil"===e||"\u2309"===e?(o="\u23a4",h=m="\u23a5",c="Size4-Regular"):"("===e||"\\lparen"===e?(o="\u239b",h="\u239c",m="\u239d",c="Size4-Regular"):")"===e||"\\rparen"===e?(o="\u239e",h="\u239f",m="\u23a0",c="Size4-Regular"):"\\{"===e||"\\lbrace"===e?(o="\u23a7",s="\u23a8",m="\u23a9",h="\u23aa",c="Size4-Regular"):"\\}"===e||"\\rbrace"===e?(o="\u23ab",s="\u23ac",m="\u23ad",h="\u23aa",c="Size4-Regular"):"\\lgroup"===e||"\u27ee"===e?(o="\u23a7",m="\u23a9",h="\u23aa",c="Size4-Regular"):"\\rgroup"===e||"\u27ef"===e?(o="\u23ab",m="\u23ad",h="\u23aa",c="Size4-Regular"):"\\lmoustache"===e||"\u23b0"===e?(o="\u23a7",m="\u23ad",h="\u23aa",c="Size4-Regular"):"\\rmoustache"!==e&&"\u23b1"!==e||(o="\u23ab",m="\u23a9",h="\u23aa",c="Size4-Regular");var u=ar(o,c,a),p=u.height+u.depth,d=ar(h,c,a),f=d.height+d.depth,g=ar(m,c,a),v=g.height+g.depth,b=0,y=1;if(null!==s){var w=ar(s,c,a);b=w.height+w.depth,y=2}var k=p+v+b,S=k+Math.max(0,Math.ceil((t-k)/(y*f)))*y*f,M=n.fontMetrics().axisHeight;r&&(M*=n.sizeMultiplier);var z=S/2-M,A=[];if(A.push(lr(m,c,a)),A.push(mr),null===s){var T=S-p-v+.016;A.push(hr(h,T,n))}else{var B=(S-p-v-b)/2+.016;A.push(hr(h,B,n)),A.push(mr),A.push(lr(s,c,a)),A.push(mr),A.push(hr(h,B,n))}A.push(mr),A.push(lr(o,c,a));var C=n.havingBaseStyle(x.TEXT),q=Ke.makeVList({positionType:"bottom",positionData:z,children:A},C);return ir(Ke.makeSpan(["delimsizing","mult"],[q],C),x.TEXT,n,i)},dr=.08,fr=function(e,t,r,n,a){var i=function(e,t,r){t*=1e3;var n="";switch(e){case"sqrtMain":n=function(e,t){return"M95,"+(622+e+t)+"\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\nc69,-144,104.5,-217.7,106.5,-221\nl"+e/2.075+" -"+e+"\nc5.3,-9.3,12,-14,20,-14\nH400000v"+(40+e)+"H845.2724\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\nM"+(834+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,M);break;case"sqrtSize1":n=function(e,t){return"M263,"+(601+e+t)+"c0.7,0,18,39.7,52,119\nc34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120\nc340,-704.7,510.7,-1060.3,512,-1067\nl"+e/2.084+" -"+e+"\nc4.7,-7.3,11,-11,19,-11\nH40000v"+(40+e)+"H1012.3\ns-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232\nc-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1\ns-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26\nc-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z\nM"+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,M);break;case"sqrtSize2":n=function(e,t){return"M983 "+(10+e+t)+"\nl"+e/3.13+" -"+e+"\nc4,-6.7,10,-10,18,-10 H400000v"+(40+e)+"\nH1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7\ns-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744\nc-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30\nc26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722\nc56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5\nc53.7,-170.3,84.5,-266.8,92.5,-289.5z\nM"+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,M);break;case"sqrtSize3":n=function(e,t){return"M424,"+(2398+e+t)+"\nc-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514\nc0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20\ns-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121\ns209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081\nl"+e/4.223+" -"+e+"c4,-6.7,10,-10,18,-10 H400000\nv"+(40+e)+"H1014.6\ns-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185\nc-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2z M"+(1001+e)+" "+t+"\nh400000v"+(40+e)+"h-400000z"}(t,M);break;case"sqrtSize4":n=function(e,t){return"M473,"+(2713+e+t)+"\nc339.3,-1799.3,509.3,-2700,510,-2702 l"+e/5.298+" -"+e+"\nc3.3,-7.3,9.3,-11,18,-11 H400000v"+(40+e)+"H1017.7\ns-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200\nc0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26\ns76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104,\n606zM"+(1001+e)+" "+t+"h400000v"+(40+e)+"H1017.7z"}(t,M);break;case"sqrtTall":n=function(e,t,r){return"M702 "+(e+t)+"H400000"+(40+e)+"\nH742v"+(r-54-t-e)+"l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1\nh-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170\nc-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667\n219 661 l218 661zM702 "+t+"H400000v"+(40+e)+"H742z"}(t,M,r)}return n}(e,n,r),o=new J(e,i),s=new K([o],{width:"400em",height:V(t),viewBox:"0 0 400000 "+r,preserveAspectRatio:"xMinYMin slice"});return Ke.makeSvgSpan(["hide-tail"],[s],a)},gr=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230a","\u230b","\\lceil","\\rceil","\u2308","\u2309","\\surd"],vr=["\\uparrow","\\downarrow","\\updownarrow","\\Uparrow","\\Downarrow","\\Updownarrow","|","\\|","\\vert","\\Vert","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27ee","\u27ef","\\lmoustache","\\rmoustache","\u23b0","\u23b1"],br=["<",">","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"],yr=[0,1.2,1.8,2.4,3],xr=[{type:"small",style:x.SCRIPTSCRIPT},{type:"small",style:x.SCRIPT},{type:"small",style:x.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],wr=[{type:"small",style:x.SCRIPTSCRIPT},{type:"small",style:x.SCRIPT},{type:"small",style:x.TEXT},{type:"stack"}],kr=[{type:"small",style:x.SCRIPTSCRIPT},{type:"small",style:x.SCRIPT},{type:"small",style:x.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],Sr=function(e){if("small"===e.type)return"Main-Regular";if("large"===e.type)return"Size"+e.size+"-Regular";if("stack"===e.type)return"Size4-Regular";throw new Error("Add support for delim type '"+e.type+"' here.")},Mr=function(e,t,r,n){for(var a=Math.min(2,3-n.style.size);at)return r[a]}return r[r.length-1]},zr=function(e,t,r,n,a,i){var o;"<"===e||"\\lt"===e||"\u27e8"===e?e="\\langle":">"!==e&&"\\gt"!==e&&"\u27e9"!==e||(e="\\rangle"),o=l.contains(br,e)?xr:l.contains(gr,e)?kr:wr;var s=Mr(e,t,o,n);return"small"===s.type?function(e,t,r,n,a,i){var o=Ke.makeSymbol(e,"Main-Regular",a,n),s=ir(o,t,n,i);return r&&or(s,n,t),s}(e,s.style,r,n,a,i):"large"===s.type?sr(e,s.size,r,n,a,i):pr(e,t,r,n,a,i)},Ar={sqrtImage:function(e,t){var r,n,a=t.havingBaseSizing(),i=Mr("\\surd",e*a.sizeMultiplier,kr,a),o=a.sizeMultiplier,s=Math.max(0,t.minRuleThickness-t.fontMetrics().sqrtRuleThickness),l=0,h=0,m=0;return"small"===i.type?(e<1?o=1:e<1.4&&(o=.7),h=(1+s)/o,(r=fr("sqrtMain",l=(1+s+dr)/o,m=1e3+1e3*s+80,s,t)).style.minWidth="0.853em",n=.833/o):"large"===i.type?(m=1080*yr[i.size],h=(yr[i.size]+s)/o,l=(yr[i.size]+s+dr)/o,(r=fr("sqrtSize"+i.size,l,m,s,t)).style.minWidth="1.02em",n=1/o):(l=e+s+dr,h=e+s,m=Math.floor(1e3*e+s)+80,(r=fr("sqrtTall",l,m,s,t)).style.minWidth="0.742em",n=1.056),r.height=h,r.style.height=V(l),{span:r,advanceWidth:n,ruleWidth:(t.fontMetrics().sqrtRuleThickness+s)*o}},sizedDelim:function(e,t,r,a,i){if("<"===e||"\\lt"===e||"\u27e8"===e?e="\\langle":">"!==e&&"\\gt"!==e&&"\u27e9"!==e||(e="\\rangle"),l.contains(gr,e)||l.contains(br,e))return sr(e,t,!1,r,a,i);if(l.contains(vr,e))return pr(e,yr[t],!1,r,a,i);throw new n("Illegal delimiter: '"+e+"'")},sizeToMaxHeight:yr,customSizedDelim:zr,leftRightDelim:function(e,t,r,n,a,i){var o=n.fontMetrics().axisHeight*n.sizeMultiplier,s=5/n.fontMetrics().ptPerEm,l=Math.max(t-o,r+o),h=Math.max(l/500*901,2*l-s);return zr(e,h,!0,n,a,i)}},Tr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},Br=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230a","\u230b","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27e8","\\rangle","\u27e9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27ee","\u27ef","\\lmoustache","\\rmoustache","\u23b0","\u23b1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."];function Cr(e,t){var r=Xt(e);if(r&&l.contains(Br,r.text))return r;throw new n(r?"Invalid delimiter '"+r.text+"' after '"+t.funcName+"'":"Invalid delimiter type '"+e.type+"'",e)}function qr(e){if(!e.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}ot({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1,argTypes:["primitive"]},handler:function(e,t){var r=Cr(t[0],e);return{type:"delimsizing",mode:e.parser.mode,size:Tr[e.funcName].size,mclass:Tr[e.funcName].mclass,delim:r.text}},htmlBuilder:function(e,t){return"."===e.delim?Ke.makeSpan([e.mclass]):Ar.sizedDelim(e.delim,e.size,t,e.mode,[e.mclass])},mathmlBuilder:function(e){var t=[];"."!==e.delim&&t.push(Bt(e.delim,e.mode));var r=new Tt.MathNode("mo",t);"mopen"===e.mclass||"mclose"===e.mclass?r.setAttribute("fence","true"):r.setAttribute("fence","false"),r.setAttribute("stretchy","true");var n=V(Ar.sizeToMaxHeight[e.size]);return r.setAttribute("minsize",n),r.setAttribute("maxsize",n),r}}),ot({type:"leftright-right",names:["\\right"],props:{numArgs:1,primitive:!0},handler:function(e,t){var r=e.parser.gullet.macros.get("\\current@color");if(r&&"string"!=typeof r)throw new n("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:e.parser.mode,delim:Cr(t[0],e).text,color:r}}}),ot({type:"leftright",names:["\\left"],props:{numArgs:1,primitive:!0},handler:function(e,t){var r=Cr(t[0],e),n=e.parser;++n.leftrightDepth;var a=n.parseExpression(!1);--n.leftrightDepth,n.expect("\\right",!1);var i=Ut(n.parseFunction(),"leftright-right");return{type:"leftright",mode:n.mode,body:a,left:r.text,right:i.delim,rightColor:i.color}},htmlBuilder:function(e,t){qr(e);for(var r,n,a=ft(e.body,t,!0,["mopen","mclose"]),i=0,o=0,s=!1,l=0;l-1?"mpadded":"menclose",[Rt(e.body,t)]);switch(e.label){case"\\cancel":n.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":n.setAttribute("notation","downdiagonalstrike");break;case"\\phase":n.setAttribute("notation","phasorangle");break;case"\\sout":n.setAttribute("notation","horizontalstrike");break;case"\\fbox":n.setAttribute("notation","box");break;case"\\angl":n.setAttribute("notation","actuarial");break;case"\\fcolorbox":case"\\colorbox":if(r=t.fontMetrics().fboxsep*t.fontMetrics().ptPerEm,n.setAttribute("width","+"+2*r+"pt"),n.setAttribute("height","+"+2*r+"pt"),n.setAttribute("lspace",r+"pt"),n.setAttribute("voffset",r+"pt"),"\\fcolorbox"===e.label){var a=Math.max(t.fontMetrics().fboxrule,t.minRuleThickness);n.setAttribute("style","border: "+a+"em solid "+String(e.borderColor))}break;case"\\xcancel":n.setAttribute("notation","updiagonalstrike downdiagonalstrike")}return e.backgroundColor&&n.setAttribute("mathbackground",e.backgroundColor),n};ot({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,argTypes:["color","text"]},handler:function(e,t,r){var n=e.parser,a=e.funcName,i=Ut(t[0],"color-token").color,o=t[1];return{type:"enclose",mode:n.mode,label:a,backgroundColor:i,body:o}},htmlBuilder:Nr,mathmlBuilder:Ir}),ot({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,argTypes:["color","color","text"]},handler:function(e,t,r){var n=e.parser,a=e.funcName,i=Ut(t[0],"color-token").color,o=Ut(t[1],"color-token").color,s=t[2];return{type:"enclose",mode:n.mode,label:a,backgroundColor:o,borderColor:i,body:s}},htmlBuilder:Nr,mathmlBuilder:Ir}),ot({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler:function(e,t){return{type:"enclose",mode:e.parser.mode,label:"\\fbox",body:t[0]}}}),ot({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\sout","\\phase"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"enclose",mode:r.mode,label:n,body:a}},htmlBuilder:Nr,mathmlBuilder:Ir}),ot({type:"enclose",names:["\\angl"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!1},handler:function(e,t){return{type:"enclose",mode:e.parser.mode,label:"\\angl",body:t[0]}}});var Rr={};function Or(e){for(var t=e.type,r=e.names,n=e.props,a=e.handler,i=e.htmlBuilder,o=e.mathmlBuilder,s={type:t,numArgs:n.numArgs||0,allowedInText:!1,numOptionalArgs:0,handler:a},l=0;l1||!c)&&g.pop(),b.length0&&(y+=.25),m.push({pos:y,isDashed:e[t]})}for(w(o[0]),r=0;r0&&(M<(B+=b)&&(M=B),B=0),e.addJot&&(M+=f),z.height=S,z.depth=M,y+=S,z.pos=y,y+=M+B,h[r]=z,w(o[r+1])}var C,q,N=y/2+t.fontMetrics().axisHeight,I=e.cols||[],R=[],O=[];if(e.tags&&e.tags.some((function(e){return e})))for(r=0;r=s)){var W=void 0;(a>0||e.hskipBeforeAndAfter)&&0!==(W=l.deflt(P.pregap,p))&&((C=Ke.makeSpan(["arraycolsep"],[])).style.width=V(W),R.push(C));var _=[];for(r=0;r0){for(var K=Ke.makeLineSpan("hline",t,c),J=Ke.makeLineSpan("hdashline",t,c),Q=[{type:"elem",elem:h,shift:0}];m.length>0;){var ee=m.pop(),te=ee.pos-N;ee.isDashed?Q.push({type:"elem",elem:J,shift:te}):Q.push({type:"elem",elem:K,shift:te})}h=Ke.makeVList({positionType:"individualShift",children:Q},t)}if(0===O.length)return Ke.makeSpan(["mord"],[h],t);var re=Ke.makeVList({positionType:"individualShift",children:O},t);return re=Ke.makeSpan(["tag"],[re],t),Ke.makeFragment([h,re])},Xr={c:"center ",l:"left ",r:"right "},Wr=function(e,t){for(var r=[],n=new Tt.MathNode("mtd",[],["mtr-glue"]),a=new Tt.MathNode("mtd",[],["mml-eqn-num"]),i=0;i0){var p=e.cols,d="",f=!1,g=0,v=p.length;"separator"===p[0].type&&(c+="top ",g=1),"separator"===p[p.length-1].type&&(c+="bottom ",v-=1);for(var b=g;b0?"left ":"",c+=S[S.length-1].length>0?"right ":"";for(var M=1;M-1?"alignat":"align",o="split"===e.envName,s=Gr(e.parser,{cols:a,addJot:!0,autoTag:o?void 0:Vr(e.envName),emptySingleRow:!0,colSeparationType:i,maxNumCols:o?2:void 0,leqno:e.parser.settings.leqno},"display"),l=0,h={type:"ordgroup",mode:e.mode,body:[]};if(t[0]&&"ordgroup"===t[0].type){for(var m="",c=0;c0&&u&&(f=1),a[p]={type:"align",align:d,pregap:f,postgap:0}}return s.colSeparationType=u?"align":"alignat",s};Or({type:"array",names:["array","darray"],props:{numArgs:1},handler:function(e,t){var r=(Xt(t[0])?[t[0]]:Ut(t[0],"ordgroup").body).map((function(e){var t=Yt(e).text;if(-1!=="lcr".indexOf(t))return{type:"align",align:t};if("|"===t)return{type:"separator",separator:"|"};if(":"===t)return{type:"separator",separator:":"};throw new n("Unknown column alignment: "+t,e)})),a={cols:r,hskipBeforeAndAfter:!0,maxNumCols:r.length};return Gr(e.parser,a,Ur(e.envName))},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix","matrix*","pmatrix*","bmatrix*","Bmatrix*","vmatrix*","Vmatrix*"],props:{numArgs:0},handler:function(e){var t={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[e.envName.replace("*","")],r="c",a={hskipBeforeAndAfter:!1,cols:[{type:"align",align:r}]};if("*"===e.envName.charAt(e.envName.length-1)){var i=e.parser;if(i.consumeSpaces(),"["===i.fetch().text){if(i.consume(),i.consumeSpaces(),r=i.fetch().text,-1==="lcr".indexOf(r))throw new n("Expected l or c or r",i.nextToken);i.consume(),i.consumeSpaces(),i.expect("]"),i.consume(),a.cols=[{type:"align",align:r}]}}var o=Gr(e.parser,a,Ur(e.envName)),s=Math.max.apply(Math,[0].concat(o.body.map((function(e){return e.length}))));return o.cols=new Array(s).fill({type:"align",align:r}),t?{type:"leftright",mode:e.mode,body:[o],left:t[0],right:t[1],rightColor:void 0}:o},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["smallmatrix"],props:{numArgs:0},handler:function(e){var t=Gr(e.parser,{arraystretch:.5},"script");return t.colSeparationType="small",t},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["subarray"],props:{numArgs:1},handler:function(e,t){var r=(Xt(t[0])?[t[0]]:Ut(t[0],"ordgroup").body).map((function(e){var t=Yt(e).text;if(-1!=="lc".indexOf(t))return{type:"align",align:t};throw new n("Unknown column alignment: "+t,e)}));if(r.length>1)throw new n("{subarray} can contain only one column");var a={cols:r,hskipBeforeAndAfter:!1,arraystretch:.5};if((a=Gr(e.parser,a,"script")).body.length>0&&a.body[0].length>1)throw new n("{subarray} can contain only one column");return a},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler:function(e){var t=Gr(e.parser,{arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},Ur(e.envName));return{type:"leftright",mode:e.mode,body:[t],left:e.envName.indexOf("r")>-1?".":"\\{",right:e.envName.indexOf("r")>-1?"\\}":".",rightColor:void 0}},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["align","align*","aligned","split"],props:{numArgs:0},handler:_r,htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["gathered","gather","gather*"],props:{numArgs:0},handler:function(e){l.contains(["gather","gather*"],e.envName)&&Fr(e);var t={cols:[{type:"align",align:"c"}],addJot:!0,colSeparationType:"gather",autoTag:Vr(e.envName),emptySingleRow:!0,leqno:e.parser.settings.leqno};return Gr(e.parser,t,"display")},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["alignat","alignat*","alignedat"],props:{numArgs:1},handler:_r,htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["equation","equation*"],props:{numArgs:0},handler:function(e){Fr(e);var t={autoTag:Vr(e.envName),emptySingleRow:!0,singleRow:!0,maxNumCols:1,leqno:e.parser.settings.leqno};return Gr(e.parser,t,"display")},htmlBuilder:Yr,mathmlBuilder:Wr}),Or({type:"array",names:["CD"],props:{numArgs:0},handler:function(e){return Fr(e),function(e){var t=[];for(e.gullet.beginGroup(),e.gullet.macros.set("\\cr","\\\\\\relax"),e.gullet.beginGroup();;){t.push(e.parseExpression(!1,"\\\\")),e.gullet.endGroup(),e.gullet.beginGroup();var r=e.fetch().text;if("&"!==r&&"\\\\"!==r){if("\\end"===r){0===t[t.length-1].length&&t.pop();break}throw new n("Expected \\\\ or \\cr or \\end",e.nextToken)}e.consume()}for(var a,i,o=[],s=[o],l=0;l-1);else{if(!("<>AV".indexOf(u)>-1))throw new n('Expected one of "<>AV=|." after @',h[c]);for(var d=0;d<2;d++){for(var f=!0,g=c+1;g=x.SCRIPT.id?r.text():x.DISPLAY:"text"===e&&r.size===x.DISPLAY.size?r=x.TEXT:"script"===e?r=x.SCRIPT:"scriptscript"===e&&(r=x.SCRIPTSCRIPT),r},nn=function(e,t){var r,n=rn(e.size,t.style),a=n.fracNum(),i=n.fracDen();r=t.havingStyle(a);var o=wt(e.numer,r,t);if(e.continued){var s=8.5/t.fontMetrics().ptPerEm,l=3.5/t.fontMetrics().ptPerEm;o.height=o.height0?3*c:7*c,d=t.fontMetrics().denom1):(m>0?(u=t.fontMetrics().num2,p=c):(u=t.fontMetrics().num3,p=3*c),d=t.fontMetrics().denom2),h){var w=t.fontMetrics().axisHeight;u-o.depth-(w+.5*m)0&&(t="."===(t=e)?null:t),t};ot({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler:function(e,t){var r,n=e.parser,a=t[4],i=t[5],o=lt(t[0]),s="atom"===o.type&&"open"===o.family?sn(o.text):null,l=lt(t[1]),h="atom"===l.type&&"close"===l.family?sn(l.text):null,m=Ut(t[2],"size"),c=null;r=!!m.isBlank||(c=m.value).number>0;var u="auto",p=t[3];if("ordgroup"===p.type){if(p.body.length>0){var d=Ut(p.body[0],"textord");u=on[Number(d.text)]}}else p=Ut(p,"textord"),u=on[Number(p.text)];return{type:"genfrac",mode:n.mode,numer:a,denom:i,continued:!1,hasBarLine:r,barSize:c,leftDelim:s,rightDelim:h,size:u}},htmlBuilder:nn,mathmlBuilder:an}),ot({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler:function(e,t){var r=e.parser,n=(e.funcName,e.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Ut(t[0],"size").value,token:n}}}),ot({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:function(e,t){var r=e.parser,n=(e.funcName,t[0]),a=function(e){if(!e)throw new Error("Expected non-null, but got "+String(e));return e}(Ut(t[1],"infix").size),i=t[2],o=a.number>0;return{type:"genfrac",mode:r.mode,numer:n,denom:i,continued:!1,hasBarLine:o,barSize:a,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:nn,mathmlBuilder:an});var ln=function(e,t){var r,n,a=t.style;"supsub"===e.type?(r=e.sup?wt(e.sup,t.havingStyle(a.sup()),t):wt(e.sub,t.havingStyle(a.sub()),t),n=Ut(e.base,"horizBrace")):n=Ut(e,"horizBrace");var i,o=wt(n.base,t.havingBaseStyle(x.DISPLAY)),s=Gt(n,t);if(n.isOver?(i=Ke.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"kern",size:.1},{type:"elem",elem:s}]},t)).children[0].children[0].children[1].classes.push("svg-align"):(i=Ke.makeVList({positionType:"bottom",positionData:o.depth+.1+s.height,children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:o}]},t)).children[0].children[0].children[0].classes.push("svg-align"),r){var l=Ke.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t);i=n.isOver?Ke.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:l},{type:"kern",size:.2},{type:"elem",elem:r}]},t):Ke.makeVList({positionType:"bottom",positionData:l.depth+.2+r.height+r.depth,children:[{type:"elem",elem:r},{type:"kern",size:.2},{type:"elem",elem:l}]},t)}return Ke.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t)};ot({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName;return{type:"horizBrace",mode:r.mode,label:n,isOver:/^\\over/.test(n),base:t[0]}},htmlBuilder:ln,mathmlBuilder:function(e,t){var r=Vt(e.label);return new Tt.MathNode(e.isOver?"mover":"munder",[Rt(e.base,t),r])}}),ot({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=t[1],a=Ut(t[0],"url").url;return r.settings.isTrusted({command:"\\href",url:a})?{type:"href",mode:r.mode,href:a,body:ht(n)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:function(e,t){var r=ft(e.body,t,!1);return Ke.makeAnchor(e.href,[],r,t)},mathmlBuilder:function(e,t){var r=It(e.body,t);return r instanceof zt||(r=new zt("mrow",[r])),r.setAttribute("href",e.href),r}}),ot({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=Ut(t[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:n}))return r.formatUnsupportedCmd("\\url");for(var a=[],i=0;i0&&(n=F(e.totalheight,t)-r);var a=0;e.width.number>0&&(a=F(e.width,t));var i={height:V(r+n)};a>0&&(i.width=V(a)),n>0&&(i.verticalAlign=V(-n));var o=new j(e.src,e.alt,i);return o.height=r,o.depth=n,o},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mglyph",[]);r.setAttribute("alt",e.alt);var n=F(e.height,t),a=0;if(e.totalheight.number>0&&(a=F(e.totalheight,t)-n,r.setAttribute("valign",V(-a))),r.setAttribute("height",V(n+a)),e.width.number>0){var i=F(e.width,t);r.setAttribute("width",V(i))}return r.setAttribute("src",e.src),r}}),ot({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=Ut(t[0],"size");if(r.settings.strict){var i="m"===n[1],o="mu"===a.value.unit;i?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" supports only mu units, not "+a.value.unit+" units"),"math"!==r.mode&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:a.value}},htmlBuilder:function(e,t){return Ke.makeGlue(e.dimension,t)},mathmlBuilder:function(e,t){var r=F(e.dimension,t);return new Tt.SpaceNode(r)}}),ot({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"lap",mode:r.mode,alignment:n.slice(5),body:a}},htmlBuilder:function(e,t){var r;"clap"===e.alignment?(r=Ke.makeSpan([],[wt(e.body,t)]),r=Ke.makeSpan(["inner"],[r],t)):r=Ke.makeSpan(["inner"],[wt(e.body,t)]);var n=Ke.makeSpan(["fix"],[]),a=Ke.makeSpan([e.alignment],[r,n],t),i=Ke.makeSpan(["strut"]);return i.style.height=V(a.height+a.depth),a.depth&&(i.style.verticalAlign=V(-a.depth)),a.children.unshift(i),a=Ke.makeSpan(["thinbox"],[a],t),Ke.makeSpan(["mord","vbox"],[a],t)},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mpadded",[Rt(e.body,t)]);if("rlap"!==e.alignment){var n="llap"===e.alignment?"-1":"-0.5";r.setAttribute("lspace",n+"width")}return r.setAttribute("width","0px"),r}}),ot({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){var r=e.funcName,n=e.parser,a=n.mode;n.switchMode("math");var i="\\("===r?"\\)":"$",o=n.parseExpression(!1,i);return n.expect(i),n.switchMode(a),{type:"styling",mode:n.mode,style:"text",body:o}}}),ot({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){throw new n("Mismatched "+e.funcName)}});var mn=function(e,t){switch(t.style.size){case x.DISPLAY.size:return e.display;case x.TEXT.size:return e.text;case x.SCRIPT.size:return e.script;case x.SCRIPTSCRIPT.size:return e.scriptscript;default:return e.text}};ot({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:function(e,t){return{type:"mathchoice",mode:e.parser.mode,display:ht(t[0]),text:ht(t[1]),script:ht(t[2]),scriptscript:ht(t[3])}},htmlBuilder:function(e,t){var r=mn(e,t),n=ft(r,t,!1);return Ke.makeFragment(n)},mathmlBuilder:function(e,t){var r=mn(e,t);return It(r,t)}});var cn=function(e,t,r,n,a,i,o){e=Ke.makeSpan([],[e]);var s,h,m,c=r&&l.isCharacterBox(r);if(t){var u=wt(t,n.havingStyle(a.sup()),n);h={elem:u,kern:Math.max(n.fontMetrics().bigOpSpacing1,n.fontMetrics().bigOpSpacing3-u.depth)}}if(r){var p=wt(r,n.havingStyle(a.sub()),n);s={elem:p,kern:Math.max(n.fontMetrics().bigOpSpacing2,n.fontMetrics().bigOpSpacing4-p.height)}}if(h&&s){var d=n.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+e.depth+o;m=Ke.makeVList({positionType:"bottom",positionData:d,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:V(-i)},{type:"kern",size:s.kern},{type:"elem",elem:e},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:V(i)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}else if(s){var f=e.height-o;m=Ke.makeVList({positionType:"top",positionData:f,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:V(-i)},{type:"kern",size:s.kern},{type:"elem",elem:e}]},n)}else{if(!h)return e;var g=e.depth+o;m=Ke.makeVList({positionType:"bottom",positionData:g,children:[{type:"elem",elem:e},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:V(i)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}var v=[m];if(s&&0!==i&&!c){var b=Ke.makeSpan(["mspace"],[],n);b.style.marginRight=V(i),v.unshift(b)}return Ke.makeSpan(["mop","op-limits"],v,n)},un=["\\smallint"],pn=function(e,t){var r,n,a,i=!1;"supsub"===e.type?(r=e.sup,n=e.sub,a=Ut(e.base,"op"),i=!0):a=Ut(e,"op");var o,s=t.style,h=!1;if(s.size===x.DISPLAY.size&&a.symbol&&!l.contains(un,a.name)&&(h=!0),a.symbol){var m=h?"Size2-Regular":"Size1-Regular",c="";if("\\oiint"!==a.name&&"\\oiiint"!==a.name||(c=a.name.substr(1),a.name="oiint"===c?"\\iint":"\\iiint"),o=Ke.makeSymbol(a.name,m,"math",t,["mop","op-symbol",h?"large-op":"small-op"]),c.length>0){var u=o.italic,p=Ke.staticSvg(c+"Size"+(h?"2":"1"),t);o=Ke.makeVList({positionType:"individualShift",children:[{type:"elem",elem:o,shift:0},{type:"elem",elem:p,shift:h?.08:0}]},t),a.name="\\"+c,o.classes.unshift("mop"),o.italic=u}}else if(a.body){var d=ft(a.body,t,!0);1===d.length&&d[0]instanceof Z?(o=d[0]).classes[0]="mop":o=Ke.makeSpan(["mop"],d,t)}else{for(var f=[],g=1;g0){for(var s=a.body.map((function(e){var t=e.text;return"string"==typeof t?{type:"textord",mode:e.mode,text:t}:e})),l=ft(s,t.withFont("mathrm"),!0),h=0;h=0?s.setAttribute("height",V(a)):(s.setAttribute("height",V(a)),s.setAttribute("depth",V(-a))),s.setAttribute("voffset",V(a)),s}});var yn=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];ot({type:"sizing",names:yn,props:{numArgs:0,allowedInText:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!1,r);return{type:"sizing",mode:a.mode,size:yn.indexOf(n)+1,body:i}},htmlBuilder:function(e,t){var r=t.havingSize(e.size);return bn(e.body,r,t)},mathmlBuilder:function(e,t){var r=t.havingSize(e.size),n=Nt(e.body,r),a=new Tt.MathNode("mstyle",n);return a.setAttribute("mathsize",V(r.sizeMultiplier)),a}}),ot({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(e,t,r){var n=e.parser,a=!1,i=!1,o=r[0]&&Ut(r[0],"ordgroup");if(o)for(var s="",l=0;lr.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=l.height-r.height-i-h;r.style.paddingLeft=V(m);var p=Ke.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+u)},{type:"elem",elem:l},{type:"kern",size:h}]},t);if(e.index){var d=t.havingStyle(x.SCRIPTSCRIPT),f=wt(e.index,d,t),g=.6*(p.height-p.depth),v=Ke.makeVList({positionType:"shift",positionData:-g,children:[{type:"elem",elem:f}]},t),b=Ke.makeSpan(["root"],[v]);return Ke.makeSpan(["mord","sqrt"],[b,p],t)}return Ke.makeSpan(["mord","sqrt"],[p],t)},mathmlBuilder:function(e,t){var r=e.body,n=e.index;return n?new Tt.MathNode("mroot",[Rt(r,t),Rt(n,t)]):new Tt.MathNode("msqrt",[Rt(r,t)])}});var xn={display:x.DISPLAY,text:x.TEXT,script:x.SCRIPT,scriptscript:x.SCRIPTSCRIPT};ot({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!0,r),o=n.slice(1,n.length-5);return{type:"styling",mode:a.mode,style:o,body:i}},htmlBuilder:function(e,t){var r=xn[e.style],n=t.havingStyle(r).withFont("");return bn(e.body,n,t)},mathmlBuilder:function(e,t){var r=xn[e.style],n=t.havingStyle(r),a=Nt(e.body,n),i=new Tt.MathNode("mstyle",a),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[e.style];return i.setAttribute("scriptlevel",o[0]),i.setAttribute("displaystyle",o[1]),i}});var wn=function(e,t){var r=e.base;return r?"op"===r.type?r.limits&&(t.style.size===x.DISPLAY.size||r.alwaysHandleSupSub)?pn:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(t.style.size===x.DISPLAY.size||r.limits)?vn:null:"accent"===r.type?l.isCharacterBox(r.base)?Wt:null:"horizBrace"===r.type&&!e.sub===r.isOver?ln:null:null};st({type:"supsub",htmlBuilder:function(e,t){var r=wn(e,t);if(r)return r(e,t);var n,a,i,o=e.base,s=e.sup,h=e.sub,m=wt(o,t),c=t.fontMetrics(),u=0,p=0,d=o&&l.isCharacterBox(o);if(s){var f=t.havingStyle(t.style.sup());n=wt(s,f,t),d||(u=m.height-f.fontMetrics().supDrop*f.sizeMultiplier/t.sizeMultiplier)}if(h){var g=t.havingStyle(t.style.sub());a=wt(h,g,t),d||(p=m.depth+g.fontMetrics().subDrop*g.sizeMultiplier/t.sizeMultiplier)}i=t.style===x.DISPLAY?c.sup1:t.style.cramped?c.sup3:c.sup2;var v,b=t.sizeMultiplier,y=V(.5/c.ptPerEm/b),w=null;if(a){var k=e.base&&"op"===e.base.type&&e.base.name&&("\\oiint"===e.base.name||"\\oiiint"===e.base.name);(m instanceof Z||k)&&(w=V(-m.italic))}if(n&&a){u=Math.max(u,i,n.depth+.25*c.xHeight),p=Math.max(p,c.sub2);var S=4*c.defaultRuleThickness;if(u-n.depth-(a.height-p)0&&(u+=M,p-=M)}var z=[{type:"elem",elem:a,shift:p,marginRight:y,marginLeft:w},{type:"elem",elem:n,shift:-u,marginRight:y}];v=Ke.makeVList({positionType:"individualShift",children:z},t)}else if(a){p=Math.max(p,c.sub1,a.height-.8*c.xHeight);var A=[{type:"elem",elem:a,marginLeft:w,marginRight:y}];v=Ke.makeVList({positionType:"shift",positionData:p,children:A},t)}else{if(!n)throw new Error("supsub must have either sup or sub.");u=Math.max(u,i,n.depth+.25*c.xHeight),v=Ke.makeVList({positionType:"shift",positionData:-u,children:[{type:"elem",elem:n,marginRight:y}]},t)}var T=yt(m,"right")||"mord";return Ke.makeSpan([T],[m,Ke.makeSpan(["msupsub"],[v])],t)},mathmlBuilder:function(e,t){var r,n=!1;e.base&&"horizBrace"===e.base.type&&!!e.sup===e.base.isOver&&(n=!0,r=e.base.isOver),!e.base||"op"!==e.base.type&&"operatorname"!==e.base.type||(e.base.parentIsSupSub=!0);var a,i=[Rt(e.base,t)];if(e.sub&&i.push(Rt(e.sub,t)),e.sup&&i.push(Rt(e.sup,t)),n)a=r?"mover":"munder";else if(e.sub)if(e.sup){var o=e.base;a=o&&"op"===o.type&&o.limits&&t.style===x.DISPLAY||o&&"operatorname"===o.type&&o.alwaysHandleSupSub&&(t.style===x.DISPLAY||o.limits)?"munderover":"msubsup"}else{var s=e.base;a=s&&"op"===s.type&&s.limits&&(t.style===x.DISPLAY||s.alwaysHandleSupSub)||s&&"operatorname"===s.type&&s.alwaysHandleSupSub&&(s.limits||t.style===x.DISPLAY)?"munder":"msub"}else{var l=e.base;a=l&&"op"===l.type&&l.limits&&(t.style===x.DISPLAY||l.alwaysHandleSupSub)||l&&"operatorname"===l.type&&l.alwaysHandleSupSub&&(l.limits||t.style===x.DISPLAY)?"mover":"msup"}return new Tt.MathNode(a,i)}}),st({type:"atom",htmlBuilder:function(e,t){return Ke.mathsym(e.text,e.mode,t,["m"+e.family])},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mo",[Bt(e.text,e.mode)]);if("bin"===e.family){var n=qt(e,t);"bold-italic"===n&&r.setAttribute("mathvariant",n)}else"punct"===e.family?r.setAttribute("separator","true"):"open"!==e.family&&"close"!==e.family||r.setAttribute("stretchy","false");return r}});var kn={mi:"italic",mn:"normal",mtext:"normal"};st({type:"mathord",htmlBuilder:function(e,t){return Ke.makeOrd(e,t,"mathord")},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mi",[Bt(e.text,e.mode,t)]),n=qt(e,t)||"italic";return n!==kn[r.type]&&r.setAttribute("mathvariant",n),r}}),st({type:"textord",htmlBuilder:function(e,t){return Ke.makeOrd(e,t,"textord")},mathmlBuilder:function(e,t){var r,n=Bt(e.text,e.mode,t),a=qt(e,t)||"normal";return r="text"===e.mode?new Tt.MathNode("mtext",[n]):/[0-9]/.test(e.text)?new Tt.MathNode("mn",[n]):"\\prime"===e.text?new Tt.MathNode("mo",[n]):new Tt.MathNode("mi",[n]),a!==kn[r.type]&&r.setAttribute("mathvariant",a),r}});var Sn={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},Mn={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};st({type:"spacing",htmlBuilder:function(e,t){if(Mn.hasOwnProperty(e.text)){var r=Mn[e.text].className||"";if("text"===e.mode){var a=Ke.makeOrd(e,t,"textord");return a.classes.push(r),a}return Ke.makeSpan(["mspace",r],[Ke.mathsym(e.text,e.mode,t)],t)}if(Sn.hasOwnProperty(e.text))return Ke.makeSpan(["mspace",Sn[e.text]],[],t);throw new n('Unknown type of space "'+e.text+'"')},mathmlBuilder:function(e,t){if(!Mn.hasOwnProperty(e.text)){if(Sn.hasOwnProperty(e.text))return new Tt.MathNode("mspace");throw new n('Unknown type of space "'+e.text+'"')}return new Tt.MathNode("mtext",[new Tt.TextNode("\xa0")])}});var zn=function(){var e=new Tt.MathNode("mtd",[]);return e.setAttribute("width","50%"),e};st({type:"tag",mathmlBuilder:function(e,t){var r=new Tt.MathNode("mtable",[new Tt.MathNode("mtr",[zn(),new Tt.MathNode("mtd",[It(e.body,t)]),zn(),new Tt.MathNode("mtd",[It(e.tag,t)])])]);return r.setAttribute("width","100%"),r}});var An={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},Tn={"\\textbf":"textbf","\\textmd":"textmd"},Bn={"\\textit":"textit","\\textup":"textup"},Cn=function(e,t){var r=e.font;return r?An[r]?t.withTextFontFamily(An[r]):Tn[r]?t.withTextFontWeight(Tn[r]):t.withTextFontShape(Bn[r]):t};ot({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"text",mode:r.mode,body:ht(a),font:n}},htmlBuilder:function(e,t){var r=Cn(e,t),n=ft(e.body,r,!0);return Ke.makeSpan(["mord","text"],n,r)},mathmlBuilder:function(e,t){var r=Cn(e,t);return It(e.body,r)}}),ot({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){return{type:"underline",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=wt(e.body,t),n=Ke.makeLineSpan("underline-line",t),a=t.fontMetrics().defaultRuleThickness,i=Ke.makeVList({positionType:"top",positionData:r.height,children:[{type:"kern",size:a},{type:"elem",elem:n},{type:"kern",size:3*a},{type:"elem",elem:r}]},t);return Ke.makeSpan(["mord","underline"],[i],t)},mathmlBuilder:function(e,t){var r=new Tt.MathNode("mo",[new Tt.TextNode("\u203e")]);r.setAttribute("stretchy","true");var n=new Tt.MathNode("munder",[Rt(e.body,t),r]);return n.setAttribute("accentunder","true"),n}}),ot({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler:function(e,t){return{type:"vcenter",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=wt(e.body,t),n=t.fontMetrics().axisHeight,a=.5*(r.height-n-(r.depth+n));return Ke.makeVList({positionType:"shift",positionData:a,children:[{type:"elem",elem:r}]},t)},mathmlBuilder:function(e,t){return new Tt.MathNode("mpadded",[Rt(e.body,t)],["vcenter"])}}),ot({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler:function(e,t,r){throw new n("\\verb ended by end of line instead of matching delimiter")},htmlBuilder:function(e,t){for(var r=qn(e),n=[],a=t.havingStyle(t.style.text()),i=0;i0;)this.endGroup()},t.has=function(e){return this.current.hasOwnProperty(e)||this.builtins.hasOwnProperty(e)},t.get=function(e){return this.current.hasOwnProperty(e)?this.current[e]:this.builtins[e]},t.set=function(e,t,r){if(void 0===r&&(r=!1),r){for(var n=0;n0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{var a=this.undefStack[this.undefStack.length-1];a&&!a.hasOwnProperty(e)&&(a[e]=this.current[e])}null==t?delete this.current[e]:this.current[e]=t},e}(),Hn=Hr;Er("\\noexpand",(function(e){var t=e.popToken();return e.isExpandable(t.text)&&(t.noexpand=!0,t.treatAsRelax=!0),{tokens:[t],numArgs:0}})),Er("\\expandafter",(function(e){var t=e.popToken();return e.expandOnce(!0),{tokens:[t],numArgs:0}})),Er("\\@firstoftwo",(function(e){return{tokens:e.consumeArgs(2)[0],numArgs:0}})),Er("\\@secondoftwo",(function(e){return{tokens:e.consumeArgs(2)[1],numArgs:0}})),Er("\\@ifnextchar",(function(e){var t=e.consumeArgs(3);e.consumeSpaces();var r=e.future();return 1===t[0].length&&t[0][0].text===r.text?{tokens:t[1],numArgs:0}:{tokens:t[2],numArgs:0}})),Er("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),Er("\\TextOrMath",(function(e){var t=e.consumeArgs(2);return"text"===e.mode?{tokens:t[0],numArgs:0}:{tokens:t[1],numArgs:0}}));var En={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};Er("\\char",(function(e){var t,r=e.popToken(),a="";if("'"===r.text)t=8,r=e.popToken();else if('"'===r.text)t=16,r=e.popToken();else if("`"===r.text)if("\\"===(r=e.popToken()).text[0])a=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new n("\\char` missing argument");a=r.text.charCodeAt(0)}else t=10;if(t){if(null==(a=En[r.text])||a>=t)throw new n("Invalid base-"+t+" digit "+r.text);for(var i;null!=(i=En[e.future().text])&&i":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};Er("\\dots",(function(e){var t="\\dotso",r=e.expandAfterFuture().text;return r in Dn?t=Dn[r]:("\\not"===r.substr(0,4)||r in ae.math&&l.contains(["bin","rel"],ae.math[r].group))&&(t="\\dotsb"),t}));var Pn={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};Er("\\dotso",(function(e){return e.future().text in Pn?"\\ldots\\,":"\\ldots"})),Er("\\dotsc",(function(e){var t=e.future().text;return t in Pn&&","!==t?"\\ldots\\,":"\\ldots"})),Er("\\cdots",(function(e){return e.future().text in Pn?"\\@cdots\\,":"\\@cdots"})),Er("\\dotsb","\\cdots"),Er("\\dotsm","\\cdots"),Er("\\dotsi","\\!\\cdots"),Er("\\dotsx","\\ldots\\,"),Er("\\DOTSI","\\relax"),Er("\\DOTSB","\\relax"),Er("\\DOTSX","\\relax"),Er("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),Er("\\,","\\tmspace+{3mu}{.1667em}"),Er("\\thinspace","\\,"),Er("\\>","\\mskip{4mu}"),Er("\\:","\\tmspace+{4mu}{.2222em}"),Er("\\medspace","\\:"),Er("\\;","\\tmspace+{5mu}{.2777em}"),Er("\\thickspace","\\;"),Er("\\!","\\tmspace-{3mu}{.1667em}"),Er("\\negthinspace","\\!"),Er("\\negmedspace","\\tmspace-{4mu}{.2222em}"),Er("\\negthickspace","\\tmspace-{5mu}{.277em}"),Er("\\enspace","\\kern.5em "),Er("\\enskip","\\hskip.5em\\relax"),Er("\\quad","\\hskip1em\\relax"),Er("\\qquad","\\hskip2em\\relax"),Er("\\tag","\\@ifstar\\tag@literal\\tag@paren"),Er("\\tag@paren","\\tag@literal{({#1})}"),Er("\\tag@literal",(function(e){if(e.macros.get("\\df@tag"))throw new n("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"})),Er("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),Er("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),Er("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),Er("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),Er("\\pmb","\\html@mathml{\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}{\\mathbf{#1}}"),Er("\\newline","\\\\\\relax"),Er("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var Fn=V(T["Main-Regular"]["T".charCodeAt(0)][1]-.7*T["Main-Regular"]["A".charCodeAt(0)][1]);Er("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+Fn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),Er("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+Fn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),Er("\\hspace","\\@ifstar\\@hspacer\\@hspace"),Er("\\@hspace","\\hskip #1\\relax"),Er("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),Er("\\ordinarycolon",":"),Er("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),Er("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),Er("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),Er("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),Er("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),Er("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),Er("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),Er("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),Er("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),Er("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),Er("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),Er("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),Er("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),Er("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),Er("\u2237","\\dblcolon"),Er("\u2239","\\eqcolon"),Er("\u2254","\\coloneqq"),Er("\u2255","\\eqqcolon"),Er("\u2a74","\\Coloneqq"),Er("\\ratio","\\vcentcolon"),Er("\\coloncolon","\\dblcolon"),Er("\\colonequals","\\coloneqq"),Er("\\coloncolonequals","\\Coloneqq"),Er("\\equalscolon","\\eqqcolon"),Er("\\equalscoloncolon","\\Eqqcolon"),Er("\\colonminus","\\coloneq"),Er("\\coloncolonminus","\\Coloneq"),Er("\\minuscolon","\\eqcolon"),Er("\\minuscoloncolon","\\Eqcolon"),Er("\\coloncolonapprox","\\Colonapprox"),Er("\\coloncolonsim","\\Colonsim"),Er("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Er("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Er("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Er("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Er("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220c}}"),Er("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),Er("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),Er("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}"),Er("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}"),Er("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}"),Er("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}"),Er("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}"),Er("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}"),Er("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}"),Er("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}"),Er("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}"),Er("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}"),Er("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}"),Er("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}"),Er("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}"),Er("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}"),Er("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}"),Er("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}"),Er("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228a}"),Er("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2acb}"),Er("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228b}"),Er("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2acc}"),Er("\\imath","\\html@mathml{\\@imath}{\u0131}"),Er("\\jmath","\\html@mathml{\\@jmath}{\u0237}"),Er("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27e6}}"),Er("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27e7}}"),Er("\u27e6","\\llbracket"),Er("\u27e7","\\rrbracket"),Er("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}"),Er("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}"),Er("\u2983","\\lBrace"),Er("\u2984","\\rBrace"),Er("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29b5}}"),Er("\u29b5","\\minuso"),Er("\\darr","\\downarrow"),Er("\\dArr","\\Downarrow"),Er("\\Darr","\\Downarrow"),Er("\\lang","\\langle"),Er("\\rang","\\rangle"),Er("\\uarr","\\uparrow"),Er("\\uArr","\\Uparrow"),Er("\\Uarr","\\Uparrow"),Er("\\N","\\mathbb{N}"),Er("\\R","\\mathbb{R}"),Er("\\Z","\\mathbb{Z}"),Er("\\alef","\\aleph"),Er("\\alefsym","\\aleph"),Er("\\Alpha","\\mathrm{A}"),Er("\\Beta","\\mathrm{B}"),Er("\\bull","\\bullet"),Er("\\Chi","\\mathrm{X}"),Er("\\clubs","\\clubsuit"),Er("\\cnums","\\mathbb{C}"),Er("\\Complex","\\mathbb{C}"),Er("\\Dagger","\\ddagger"),Er("\\diamonds","\\diamondsuit"),Er("\\empty","\\emptyset"),Er("\\Epsilon","\\mathrm{E}"),Er("\\Eta","\\mathrm{H}"),Er("\\exist","\\exists"),Er("\\harr","\\leftrightarrow"),Er("\\hArr","\\Leftrightarrow"),Er("\\Harr","\\Leftrightarrow"),Er("\\hearts","\\heartsuit"),Er("\\image","\\Im"),Er("\\infin","\\infty"),Er("\\Iota","\\mathrm{I}"),Er("\\isin","\\in"),Er("\\Kappa","\\mathrm{K}"),Er("\\larr","\\leftarrow"),Er("\\lArr","\\Leftarrow"),Er("\\Larr","\\Leftarrow"),Er("\\lrarr","\\leftrightarrow"),Er("\\lrArr","\\Leftrightarrow"),Er("\\Lrarr","\\Leftrightarrow"),Er("\\Mu","\\mathrm{M}"),Er("\\natnums","\\mathbb{N}"),Er("\\Nu","\\mathrm{N}"),Er("\\Omicron","\\mathrm{O}"),Er("\\plusmn","\\pm"),Er("\\rarr","\\rightarrow"),Er("\\rArr","\\Rightarrow"),Er("\\Rarr","\\Rightarrow"),Er("\\real","\\Re"),Er("\\reals","\\mathbb{R}"),Er("\\Reals","\\mathbb{R}"),Er("\\Rho","\\mathrm{P}"),Er("\\sdot","\\cdot"),Er("\\sect","\\S"),Er("\\spades","\\spadesuit"),Er("\\sub","\\subset"),Er("\\sube","\\subseteq"),Er("\\supe","\\supseteq"),Er("\\Tau","\\mathrm{T}"),Er("\\thetasym","\\vartheta"),Er("\\weierp","\\wp"),Er("\\Zeta","\\mathrm{Z}"),Er("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),Er("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),Er("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),Er("\\bra","\\mathinner{\\langle{#1}|}"),Er("\\ket","\\mathinner{|{#1}\\rangle}"),Er("\\braket","\\mathinner{\\langle{#1}\\rangle}"),Er("\\Bra","\\left\\langle#1\\right|"),Er("\\Ket","\\left|#1\\right\\rangle");var Vn=function(e){return function(t){var r=t.consumeArg().tokens,n=t.consumeArg().tokens,a=t.consumeArg().tokens,i=t.consumeArg().tokens,o=t.macros.get("|"),s=t.macros.get("\\|");t.macros.beginGroup();var l=function(t){return function(r){e&&(r.macros.set("|",o),a.length&&r.macros.set("\\|",s));var i=t;!t&&a.length&&("|"===r.future().text&&(r.popToken(),i=!0));return{tokens:i?a:n,numArgs:0}}};t.macros.set("|",l(!1)),a.length&&t.macros.set("\\|",l(!0));var h=t.consumeArg().tokens,m=t.expandTokens([].concat(i,h,r));return t.macros.endGroup(),{tokens:m.reverse(),numArgs:0}}};Er("\\bra@ket",Vn(!1)),Er("\\bra@set",Vn(!0)),Er("\\Braket","\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}"),Er("\\Set","\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}"),Er("\\set","\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}"),Er("\\angln","{\\angl n}"),Er("\\blue","\\textcolor{##6495ed}{#1}"),Er("\\orange","\\textcolor{##ffa500}{#1}"),Er("\\pink","\\textcolor{##ff00af}{#1}"),Er("\\red","\\textcolor{##df0030}{#1}"),Er("\\green","\\textcolor{##28ae7b}{#1}"),Er("\\gray","\\textcolor{gray}{#1}"),Er("\\purple","\\textcolor{##9d38bd}{#1}"),Er("\\blueA","\\textcolor{##ccfaff}{#1}"),Er("\\blueB","\\textcolor{##80f6ff}{#1}"),Er("\\blueC","\\textcolor{##63d9ea}{#1}"),Er("\\blueD","\\textcolor{##11accd}{#1}"),Er("\\blueE","\\textcolor{##0c7f99}{#1}"),Er("\\tealA","\\textcolor{##94fff5}{#1}"),Er("\\tealB","\\textcolor{##26edd5}{#1}"),Er("\\tealC","\\textcolor{##01d1c1}{#1}"),Er("\\tealD","\\textcolor{##01a995}{#1}"),Er("\\tealE","\\textcolor{##208170}{#1}"),Er("\\greenA","\\textcolor{##b6ffb0}{#1}"),Er("\\greenB","\\textcolor{##8af281}{#1}"),Er("\\greenC","\\textcolor{##74cf70}{#1}"),Er("\\greenD","\\textcolor{##1fab54}{#1}"),Er("\\greenE","\\textcolor{##0d923f}{#1}"),Er("\\goldA","\\textcolor{##ffd0a9}{#1}"),Er("\\goldB","\\textcolor{##ffbb71}{#1}"),Er("\\goldC","\\textcolor{##ff9c39}{#1}"),Er("\\goldD","\\textcolor{##e07d10}{#1}"),Er("\\goldE","\\textcolor{##a75a05}{#1}"),Er("\\redA","\\textcolor{##fca9a9}{#1}"),Er("\\redB","\\textcolor{##ff8482}{#1}"),Er("\\redC","\\textcolor{##f9685d}{#1}"),Er("\\redD","\\textcolor{##e84d39}{#1}"),Er("\\redE","\\textcolor{##bc2612}{#1}"),Er("\\maroonA","\\textcolor{##ffbde0}{#1}"),Er("\\maroonB","\\textcolor{##ff92c6}{#1}"),Er("\\maroonC","\\textcolor{##ed5fa6}{#1}"),Er("\\maroonD","\\textcolor{##ca337c}{#1}"),Er("\\maroonE","\\textcolor{##9e034e}{#1}"),Er("\\purpleA","\\textcolor{##ddd7ff}{#1}"),Er("\\purpleB","\\textcolor{##c6b9fc}{#1}"),Er("\\purpleC","\\textcolor{##aa87ff}{#1}"),Er("\\purpleD","\\textcolor{##7854ab}{#1}"),Er("\\purpleE","\\textcolor{##543b78}{#1}"),Er("\\mintA","\\textcolor{##f5f9e8}{#1}"),Er("\\mintB","\\textcolor{##edf2df}{#1}"),Er("\\mintC","\\textcolor{##e0e5cc}{#1}"),Er("\\grayA","\\textcolor{##f6f7f7}{#1}"),Er("\\grayB","\\textcolor{##f0f1f2}{#1}"),Er("\\grayC","\\textcolor{##e3e5e6}{#1}"),Er("\\grayD","\\textcolor{##d6d8da}{#1}"),Er("\\grayE","\\textcolor{##babec2}{#1}"),Er("\\grayF","\\textcolor{##888d93}{#1}"),Er("\\grayG","\\textcolor{##626569}{#1}"),Er("\\grayH","\\textcolor{##3b3e40}{#1}"),Er("\\grayI","\\textcolor{##21242c}{#1}"),Er("\\kaBlue","\\textcolor{##314453}{#1}"),Er("\\kaGreen","\\textcolor{##71B307}{#1}");var Gn={"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},Un=function(){function e(e,t,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new On(Hn,t.macros),this.mode=r,this.stack=[]}var t=e.prototype;return t.feed=function(e){this.lexer=new Rn(e,this.settings)},t.switchMode=function(e){this.mode=e},t.beginGroup=function(){this.macros.beginGroup()},t.endGroup=function(){this.macros.endGroup()},t.endGroups=function(){this.macros.endGroups()},t.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},t.popToken=function(){return this.future(),this.stack.pop()},t.pushToken=function(e){this.stack.push(e)},t.pushTokens=function(e){var t;(t=this.stack).push.apply(t,e)},t.scanArgument=function(e){var t,r,n;if(e){if(this.consumeSpaces(),"["!==this.future().text)return null;t=this.popToken();var a=this.consumeArg(["]"]);n=a.tokens,r=a.end}else{var i=this.consumeArg();n=i.tokens,t=i.start,r=i.end}return this.pushToken(new Dr("EOF",r.loc)),this.pushTokens(n),t.range(r,"")},t.consumeSpaces=function(){for(;;){if(" "!==this.future().text)break;this.stack.pop()}},t.consumeArg=function(e){var t=[],r=e&&e.length>0;r||this.consumeSpaces();var a,i=this.future(),o=0,s=0;do{if(a=this.popToken(),t.push(a),"{"===a.text)++o;else if("}"===a.text){if(-1===--o)throw new n("Extra }",a)}else if("EOF"===a.text)throw new n("Unexpected end of input in a macro argument, expected '"+(e&&r?e[s]:"}")+"'",a);if(e&&r)if((0===o||1===o&&"{"===e[s])&&a.text===e[s]){if(++s===e.length){t.splice(-s,s);break}}else s=0}while(0!==o||r);return"{"===i.text&&"}"===t[t.length-1].text&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:i,end:a}},t.consumeArgs=function(e,t){if(t){if(t.length!==e+1)throw new n("The length of delimiters doesn't match the number of args!");for(var r=t[0],a=0;athis.settings.maxExpand)throw new n("Too many expansions: infinite loop or need to increase maxExpand setting");var i=a.tokens,o=this.consumeArgs(a.numArgs,a.delimiters);if(a.numArgs)for(var s=(i=i.slice()).length-1;s>=0;--s){var l=i[s];if("#"===l.text){if(0===s)throw new n("Incomplete placeholder at end of macro body",l);if("#"===(l=i[--s]).text)i.splice(s+1,1);else{if(!/^[1-9]$/.test(l.text))throw new n("Not a valid argument number",l);var h;(h=i).splice.apply(h,[s,2].concat(o[+l.text-1]))}}}return this.pushTokens(i),i},t.expandAfterFuture=function(){return this.expandOnce(),this.future()},t.expandNextToken=function(){for(;;){var e=this.expandOnce();if(e instanceof Dr)return e.treatAsRelax&&(e.text="\\relax"),this.stack.pop()}throw new Error},t.expandMacro=function(e){return this.macros.has(e)?this.expandTokens([new Dr(e)]):void 0},t.expandTokens=function(e){var t=[],r=this.stack.length;for(this.pushTokens(e);this.stack.length>r;){var n=this.expandOnce(!0);n instanceof Dr&&(n.treatAsRelax&&(n.noexpand=!1,n.treatAsRelax=!1),t.push(this.stack.pop()))}return t},t.expandMacroAsText=function(e){var t=this.expandMacro(e);return t?t.map((function(e){return e.text})).join(""):t},t._getExpansion=function(e){var t=this.macros.get(e);if(null==t)return t;if(1===e.length){var r=this.lexer.catcodes[e];if(null!=r&&13!==r)return}var n="function"==typeof t?t(this):t;if("string"==typeof n){var a=0;if(-1!==n.indexOf("#"))for(var i=n.replace(/##/g,"");-1!==i.indexOf("#"+(a+1));)++a;for(var o=new Rn(n,this.settings),s=[],l=o.lex();"EOF"!==l.text;)s.push(l),l=o.lex();return s.reverse(),{tokens:s,numArgs:a}}return n},t.isDefined=function(e){return this.macros.has(e)||Nn.hasOwnProperty(e)||ae.math.hasOwnProperty(e)||ae.text.hasOwnProperty(e)||Gn.hasOwnProperty(e)},t.isExpandable=function(e){var t=this.macros.get(e);return null!=t?"string"==typeof t||"function"==typeof t||!t.unexpandable:Nn.hasOwnProperty(e)&&!Nn[e].primitive},e}(),Yn=/^[\u208a\u208b\u208c\u208d\u208e\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\u2090\u2091\u2095\u1d62\u2c7c\u2096\u2097\u2098\u2099\u2092\u209a\u1d63\u209b\u209c\u1d64\u1d65\u2093\u1d66\u1d67\u1d68\u1d69\u1d6a]/,Xn=Object.freeze({"\u208a":"+","\u208b":"-","\u208c":"=","\u208d":"(","\u208e":")","\u2080":"0","\u2081":"1","\u2082":"2","\u2083":"3","\u2084":"4","\u2085":"5","\u2086":"6","\u2087":"7","\u2088":"8","\u2089":"9","\u2090":"a","\u2091":"e","\u2095":"h","\u1d62":"i","\u2c7c":"j","\u2096":"k","\u2097":"l","\u2098":"m","\u2099":"n","\u2092":"o","\u209a":"p","\u1d63":"r","\u209b":"s","\u209c":"t","\u1d64":"u","\u1d65":"v","\u2093":"x","\u1d66":"\u03b2","\u1d67":"\u03b3","\u1d68":"\u03c1","\u1d69":"\u03d5","\u1d6a":"\u03c7","\u207a":"+","\u207b":"-","\u207c":"=","\u207d":"(","\u207e":")","\u2070":"0","\xb9":"1","\xb2":"2","\xb3":"3","\u2074":"4","\u2075":"5","\u2076":"6","\u2077":"7","\u2078":"8","\u2079":"9","\u1d2c":"A","\u1d2e":"B","\u1d30":"D","\u1d31":"E","\u1d33":"G","\u1d34":"H","\u1d35":"I","\u1d36":"J","\u1d37":"K","\u1d38":"L","\u1d39":"M","\u1d3a":"N","\u1d3c":"O","\u1d3e":"P","\u1d3f":"R","\u1d40":"T","\u1d41":"U","\u2c7d":"V","\u1d42":"W","\u1d43":"a","\u1d47":"b","\u1d9c":"c","\u1d48":"d","\u1d49":"e","\u1da0":"f","\u1d4d":"g","\u02b0":"h","\u2071":"i","\u02b2":"j","\u1d4f":"k","\u02e1":"l","\u1d50":"m","\u207f":"n","\u1d52":"o","\u1d56":"p","\u02b3":"r","\u02e2":"s","\u1d57":"t","\u1d58":"u","\u1d5b":"v","\u02b7":"w","\u02e3":"x","\u02b8":"y","\u1dbb":"z","\u1d5d":"\u03b2","\u1d5e":"\u03b3","\u1d5f":"\u03b4","\u1d60":"\u03d5","\u1d61":"\u03c7","\u1dbf":"\u03b8"}),Wn={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030c":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030a":{text:"\\r",math:"\\mathring"},"\u030b":{text:"\\H"},"\u0327":{text:"\\c"}},_n={"\xe1":"a\u0301","\xe0":"a\u0300","\xe4":"a\u0308","\u01df":"a\u0308\u0304","\xe3":"a\u0303","\u0101":"a\u0304","\u0103":"a\u0306","\u1eaf":"a\u0306\u0301","\u1eb1":"a\u0306\u0300","\u1eb5":"a\u0306\u0303","\u01ce":"a\u030c","\xe2":"a\u0302","\u1ea5":"a\u0302\u0301","\u1ea7":"a\u0302\u0300","\u1eab":"a\u0302\u0303","\u0227":"a\u0307","\u01e1":"a\u0307\u0304","\xe5":"a\u030a","\u01fb":"a\u030a\u0301","\u1e03":"b\u0307","\u0107":"c\u0301","\u1e09":"c\u0327\u0301","\u010d":"c\u030c","\u0109":"c\u0302","\u010b":"c\u0307","\xe7":"c\u0327","\u010f":"d\u030c","\u1e0b":"d\u0307","\u1e11":"d\u0327","\xe9":"e\u0301","\xe8":"e\u0300","\xeb":"e\u0308","\u1ebd":"e\u0303","\u0113":"e\u0304","\u1e17":"e\u0304\u0301","\u1e15":"e\u0304\u0300","\u0115":"e\u0306","\u1e1d":"e\u0327\u0306","\u011b":"e\u030c","\xea":"e\u0302","\u1ebf":"e\u0302\u0301","\u1ec1":"e\u0302\u0300","\u1ec5":"e\u0302\u0303","\u0117":"e\u0307","\u0229":"e\u0327","\u1e1f":"f\u0307","\u01f5":"g\u0301","\u1e21":"g\u0304","\u011f":"g\u0306","\u01e7":"g\u030c","\u011d":"g\u0302","\u0121":"g\u0307","\u0123":"g\u0327","\u1e27":"h\u0308","\u021f":"h\u030c","\u0125":"h\u0302","\u1e23":"h\u0307","\u1e29":"h\u0327","\xed":"i\u0301","\xec":"i\u0300","\xef":"i\u0308","\u1e2f":"i\u0308\u0301","\u0129":"i\u0303","\u012b":"i\u0304","\u012d":"i\u0306","\u01d0":"i\u030c","\xee":"i\u0302","\u01f0":"j\u030c","\u0135":"j\u0302","\u1e31":"k\u0301","\u01e9":"k\u030c","\u0137":"k\u0327","\u013a":"l\u0301","\u013e":"l\u030c","\u013c":"l\u0327","\u1e3f":"m\u0301","\u1e41":"m\u0307","\u0144":"n\u0301","\u01f9":"n\u0300","\xf1":"n\u0303","\u0148":"n\u030c","\u1e45":"n\u0307","\u0146":"n\u0327","\xf3":"o\u0301","\xf2":"o\u0300","\xf6":"o\u0308","\u022b":"o\u0308\u0304","\xf5":"o\u0303","\u1e4d":"o\u0303\u0301","\u1e4f":"o\u0303\u0308","\u022d":"o\u0303\u0304","\u014d":"o\u0304","\u1e53":"o\u0304\u0301","\u1e51":"o\u0304\u0300","\u014f":"o\u0306","\u01d2":"o\u030c","\xf4":"o\u0302","\u1ed1":"o\u0302\u0301","\u1ed3":"o\u0302\u0300","\u1ed7":"o\u0302\u0303","\u022f":"o\u0307","\u0231":"o\u0307\u0304","\u0151":"o\u030b","\u1e55":"p\u0301","\u1e57":"p\u0307","\u0155":"r\u0301","\u0159":"r\u030c","\u1e59":"r\u0307","\u0157":"r\u0327","\u015b":"s\u0301","\u1e65":"s\u0301\u0307","\u0161":"s\u030c","\u1e67":"s\u030c\u0307","\u015d":"s\u0302","\u1e61":"s\u0307","\u015f":"s\u0327","\u1e97":"t\u0308","\u0165":"t\u030c","\u1e6b":"t\u0307","\u0163":"t\u0327","\xfa":"u\u0301","\xf9":"u\u0300","\xfc":"u\u0308","\u01d8":"u\u0308\u0301","\u01dc":"u\u0308\u0300","\u01d6":"u\u0308\u0304","\u01da":"u\u0308\u030c","\u0169":"u\u0303","\u1e79":"u\u0303\u0301","\u016b":"u\u0304","\u1e7b":"u\u0304\u0308","\u016d":"u\u0306","\u01d4":"u\u030c","\xfb":"u\u0302","\u016f":"u\u030a","\u0171":"u\u030b","\u1e7d":"v\u0303","\u1e83":"w\u0301","\u1e81":"w\u0300","\u1e85":"w\u0308","\u0175":"w\u0302","\u1e87":"w\u0307","\u1e98":"w\u030a","\u1e8d":"x\u0308","\u1e8b":"x\u0307","\xfd":"y\u0301","\u1ef3":"y\u0300","\xff":"y\u0308","\u1ef9":"y\u0303","\u0233":"y\u0304","\u0177":"y\u0302","\u1e8f":"y\u0307","\u1e99":"y\u030a","\u017a":"z\u0301","\u017e":"z\u030c","\u1e91":"z\u0302","\u017c":"z\u0307","\xc1":"A\u0301","\xc0":"A\u0300","\xc4":"A\u0308","\u01de":"A\u0308\u0304","\xc3":"A\u0303","\u0100":"A\u0304","\u0102":"A\u0306","\u1eae":"A\u0306\u0301","\u1eb0":"A\u0306\u0300","\u1eb4":"A\u0306\u0303","\u01cd":"A\u030c","\xc2":"A\u0302","\u1ea4":"A\u0302\u0301","\u1ea6":"A\u0302\u0300","\u1eaa":"A\u0302\u0303","\u0226":"A\u0307","\u01e0":"A\u0307\u0304","\xc5":"A\u030a","\u01fa":"A\u030a\u0301","\u1e02":"B\u0307","\u0106":"C\u0301","\u1e08":"C\u0327\u0301","\u010c":"C\u030c","\u0108":"C\u0302","\u010a":"C\u0307","\xc7":"C\u0327","\u010e":"D\u030c","\u1e0a":"D\u0307","\u1e10":"D\u0327","\xc9":"E\u0301","\xc8":"E\u0300","\xcb":"E\u0308","\u1ebc":"E\u0303","\u0112":"E\u0304","\u1e16":"E\u0304\u0301","\u1e14":"E\u0304\u0300","\u0114":"E\u0306","\u1e1c":"E\u0327\u0306","\u011a":"E\u030c","\xca":"E\u0302","\u1ebe":"E\u0302\u0301","\u1ec0":"E\u0302\u0300","\u1ec4":"E\u0302\u0303","\u0116":"E\u0307","\u0228":"E\u0327","\u1e1e":"F\u0307","\u01f4":"G\u0301","\u1e20":"G\u0304","\u011e":"G\u0306","\u01e6":"G\u030c","\u011c":"G\u0302","\u0120":"G\u0307","\u0122":"G\u0327","\u1e26":"H\u0308","\u021e":"H\u030c","\u0124":"H\u0302","\u1e22":"H\u0307","\u1e28":"H\u0327","\xcd":"I\u0301","\xcc":"I\u0300","\xcf":"I\u0308","\u1e2e":"I\u0308\u0301","\u0128":"I\u0303","\u012a":"I\u0304","\u012c":"I\u0306","\u01cf":"I\u030c","\xce":"I\u0302","\u0130":"I\u0307","\u0134":"J\u0302","\u1e30":"K\u0301","\u01e8":"K\u030c","\u0136":"K\u0327","\u0139":"L\u0301","\u013d":"L\u030c","\u013b":"L\u0327","\u1e3e":"M\u0301","\u1e40":"M\u0307","\u0143":"N\u0301","\u01f8":"N\u0300","\xd1":"N\u0303","\u0147":"N\u030c","\u1e44":"N\u0307","\u0145":"N\u0327","\xd3":"O\u0301","\xd2":"O\u0300","\xd6":"O\u0308","\u022a":"O\u0308\u0304","\xd5":"O\u0303","\u1e4c":"O\u0303\u0301","\u1e4e":"O\u0303\u0308","\u022c":"O\u0303\u0304","\u014c":"O\u0304","\u1e52":"O\u0304\u0301","\u1e50":"O\u0304\u0300","\u014e":"O\u0306","\u01d1":"O\u030c","\xd4":"O\u0302","\u1ed0":"O\u0302\u0301","\u1ed2":"O\u0302\u0300","\u1ed6":"O\u0302\u0303","\u022e":"O\u0307","\u0230":"O\u0307\u0304","\u0150":"O\u030b","\u1e54":"P\u0301","\u1e56":"P\u0307","\u0154":"R\u0301","\u0158":"R\u030c","\u1e58":"R\u0307","\u0156":"R\u0327","\u015a":"S\u0301","\u1e64":"S\u0301\u0307","\u0160":"S\u030c","\u1e66":"S\u030c\u0307","\u015c":"S\u0302","\u1e60":"S\u0307","\u015e":"S\u0327","\u0164":"T\u030c","\u1e6a":"T\u0307","\u0162":"T\u0327","\xda":"U\u0301","\xd9":"U\u0300","\xdc":"U\u0308","\u01d7":"U\u0308\u0301","\u01db":"U\u0308\u0300","\u01d5":"U\u0308\u0304","\u01d9":"U\u0308\u030c","\u0168":"U\u0303","\u1e78":"U\u0303\u0301","\u016a":"U\u0304","\u1e7a":"U\u0304\u0308","\u016c":"U\u0306","\u01d3":"U\u030c","\xdb":"U\u0302","\u016e":"U\u030a","\u0170":"U\u030b","\u1e7c":"V\u0303","\u1e82":"W\u0301","\u1e80":"W\u0300","\u1e84":"W\u0308","\u0174":"W\u0302","\u1e86":"W\u0307","\u1e8c":"X\u0308","\u1e8a":"X\u0307","\xdd":"Y\u0301","\u1ef2":"Y\u0300","\u0178":"Y\u0308","\u1ef8":"Y\u0303","\u0232":"Y\u0304","\u0176":"Y\u0302","\u1e8e":"Y\u0307","\u0179":"Z\u0301","\u017d":"Z\u030c","\u1e90":"Z\u0302","\u017b":"Z\u0307","\u03ac":"\u03b1\u0301","\u1f70":"\u03b1\u0300","\u1fb1":"\u03b1\u0304","\u1fb0":"\u03b1\u0306","\u03ad":"\u03b5\u0301","\u1f72":"\u03b5\u0300","\u03ae":"\u03b7\u0301","\u1f74":"\u03b7\u0300","\u03af":"\u03b9\u0301","\u1f76":"\u03b9\u0300","\u03ca":"\u03b9\u0308","\u0390":"\u03b9\u0308\u0301","\u1fd2":"\u03b9\u0308\u0300","\u1fd1":"\u03b9\u0304","\u1fd0":"\u03b9\u0306","\u03cc":"\u03bf\u0301","\u1f78":"\u03bf\u0300","\u03cd":"\u03c5\u0301","\u1f7a":"\u03c5\u0300","\u03cb":"\u03c5\u0308","\u03b0":"\u03c5\u0308\u0301","\u1fe2":"\u03c5\u0308\u0300","\u1fe1":"\u03c5\u0304","\u1fe0":"\u03c5\u0306","\u03ce":"\u03c9\u0301","\u1f7c":"\u03c9\u0300","\u038e":"\u03a5\u0301","\u1fea":"\u03a5\u0300","\u03ab":"\u03a5\u0308","\u1fe9":"\u03a5\u0304","\u1fe8":"\u03a5\u0306","\u038f":"\u03a9\u0301","\u1ffa":"\u03a9\u0300"},jn=function(){function e(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new Un(e,t,this.mode),this.settings=t,this.leftrightDepth=0}var t=e.prototype;return t.expect=function(e,t){if(void 0===t&&(t=!0),this.fetch().text!==e)throw new n("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()},t.consume=function(){this.nextToken=null},t.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},t.switchMode=function(e){this.mode=e,this.gullet.switchMode(e)},t.parse=function(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");try{var e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e}finally{this.gullet.endGroups()}},t.subparse=function(e){var t=this.nextToken;this.consume(),this.gullet.pushToken(new Dr("}")),this.gullet.pushTokens(e);var r=this.parseExpression(!1);return this.expect("}"),this.nextToken=t,r},t.parseExpression=function(t,r){for(var n=[];;){"math"===this.mode&&this.consumeSpaces();var a=this.fetch();if(-1!==e.endOfExpression.indexOf(a.text))break;if(r&&a.text===r)break;if(t&&Nn[a.text]&&Nn[a.text].infix)break;var i=this.parseAtom(r);if(!i)break;"internal"!==i.type&&n.push(i)}return"text"===this.mode&&this.formLigatures(n),this.handleInfixNodes(n)},t.handleInfixNodes=function(e){for(var t,r=-1,a=0;a=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+t[0]+'" used in math mode',e);var s,l=ae[this.mode][t].group,h=Lr.range(e);if(te.hasOwnProperty(l)){var m=l;s={type:"atom",mode:this.mode,family:m,loc:h,text:t}}else s={type:l,mode:this.mode,loc:h,text:t};i=s}else{if(!(t.charCodeAt(0)>=128))return null;this.settings.strict&&(S(t.charCodeAt(0))?"math"===this.mode&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+t[0]+'" used in math mode',e):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+t[0]+'" ('+t.charCodeAt(0)+")",e)),i={type:"textord",mode:"text",loc:Lr.range(e),text:t}}if(this.consume(),o)for(var c=0;c0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/logo-simple.svg b/_static/logo-simple.svg new file mode 100644 index 0000000000..b2f644b0c9 --- /dev/null +++ b/_static/logo-simple.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000000..d96755fdaf Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/nbsphinx-broken-thumbnail.svg b/_static/nbsphinx-broken-thumbnail.svg new file mode 100644 index 0000000000..4919ca8829 --- /dev/null +++ b/_static/nbsphinx-broken-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/_static/nbsphinx-code-cells.css b/_static/nbsphinx-code-cells.css new file mode 100644 index 0000000000..a3fb27c30f --- /dev/null +++ b/_static/nbsphinx-code-cells.css @@ -0,0 +1,259 @@ +/* remove conflicting styling from Sphinx themes */ +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt *, +div.nbinput.container div.input_area pre, +div.nboutput.container div.output_area pre, +div.nbinput.container div.input_area .highlight, +div.nboutput.container div.output_area .highlight { + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +div.nbinput.container > div[class*=highlight], +div.nboutput.container > div[class*=highlight] { + margin: 0; +} + +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt * { + background: none; +} + +div.nboutput.container div.output_area .highlight, +div.nboutput.container div.output_area pre { + background: unset; +} + +div.nboutput.container div.output_area div.highlight { + color: unset; /* override Pygments text color */ +} + +/* avoid gaps between output lines */ +div.nboutput.container div[class*=highlight] pre { + line-height: normal; +} + +/* input/output containers */ +div.nbinput.container, +div.nboutput.container { + display: -webkit-flex; + display: flex; + align-items: flex-start; + margin: 0; + width: 100%; +} +@media (max-width: 540px) { + div.nbinput.container, + div.nboutput.container { + flex-direction: column; + } +} + +/* input container */ +div.nbinput.container { + padding-top: 5px; +} + +/* last container */ +div.nblast.container { + padding-bottom: 5px; +} + +/* input prompt */ +div.nbinput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nbinput.container div.prompt pre > code { + color: #307FC1; +} + +/* output prompt */ +div.nboutput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nboutput.container div.prompt pre > code { + color: #BF5B3D; +} + +/* all prompts */ +div.nbinput.container div.prompt, +div.nboutput.container div.prompt { + width: 4.5ex; + padding-top: 5px; + position: relative; + user-select: none; +} + +div.nbinput.container div.prompt > div, +div.nboutput.container div.prompt > div { + position: absolute; + right: 0; + margin-right: 0.3ex; +} + +@media (max-width: 540px) { + div.nbinput.container div.prompt, + div.nboutput.container div.prompt { + width: unset; + text-align: left; + padding: 0.4em; + } + div.nboutput.container div.prompt.empty { + padding: 0; + } + + div.nbinput.container div.prompt > div, + div.nboutput.container div.prompt > div { + position: unset; + } +} + +/* disable scrollbars and line breaks on prompts */ +div.nbinput.container div.prompt pre, +div.nboutput.container div.prompt pre { + overflow: hidden; + white-space: pre; +} + +/* input/output area */ +div.nbinput.container div.input_area, +div.nboutput.container div.output_area { + -webkit-flex: 1; + flex: 1; + overflow: auto; +} +@media (max-width: 540px) { + div.nbinput.container div.input_area, + div.nboutput.container div.output_area { + width: 100%; + } +} + +/* input area */ +div.nbinput.container div.input_area { + border: 1px solid #e0e0e0; + border-radius: 2px; + /*background: #f5f5f5;*/ +} + +/* override MathJax center alignment in output cells */ +div.nboutput.container div[class*=MathJax] { + text-align: left !important; +} + +/* override sphinx.ext.imgmath center alignment in output cells */ +div.nboutput.container div.math p { + text-align: left; +} + +/* standard error */ +div.nboutput.container div.output_area.stderr { + background: #fdd; +} + +/* ANSI colors */ +.ansi-black-fg { color: #3E424D; } +.ansi-black-bg { background-color: #3E424D; } +.ansi-black-intense-fg { color: #282C36; } +.ansi-black-intense-bg { background-color: #282C36; } +.ansi-red-fg { color: #E75C58; } +.ansi-red-bg { background-color: #E75C58; } +.ansi-red-intense-fg { color: #B22B31; } +.ansi-red-intense-bg { background-color: #B22B31; } +.ansi-green-fg { color: #00A250; } +.ansi-green-bg { background-color: #00A250; } +.ansi-green-intense-fg { color: #007427; } +.ansi-green-intense-bg { background-color: #007427; } +.ansi-yellow-fg { color: #DDB62B; } +.ansi-yellow-bg { background-color: #DDB62B; } +.ansi-yellow-intense-fg { color: #B27D12; } +.ansi-yellow-intense-bg { background-color: #B27D12; } +.ansi-blue-fg { color: #208FFB; } +.ansi-blue-bg { background-color: #208FFB; } +.ansi-blue-intense-fg { color: #0065CA; } +.ansi-blue-intense-bg { background-color: #0065CA; } +.ansi-magenta-fg { color: #D160C4; } +.ansi-magenta-bg { background-color: #D160C4; } +.ansi-magenta-intense-fg { color: #A03196; } +.ansi-magenta-intense-bg { background-color: #A03196; } +.ansi-cyan-fg { color: #60C6C8; } +.ansi-cyan-bg { background-color: #60C6C8; } +.ansi-cyan-intense-fg { color: #258F8F; } +.ansi-cyan-intense-bg { background-color: #258F8F; } +.ansi-white-fg { color: #C5C1B4; } +.ansi-white-bg { background-color: #C5C1B4; } +.ansi-white-intense-fg { color: #A1A6B2; } +.ansi-white-intense-bg { background-color: #A1A6B2; } + +.ansi-default-inverse-fg { color: #FFFFFF; } +.ansi-default-inverse-bg { background-color: #000000; } + +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } + + +div.nbinput.container div.input_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight].math, +div.nboutput.container div.output_area.rendered_html, +div.nboutput.container div.output_area > div.output_javascript, +div.nboutput.container div.output_area:not(.rendered_html) > img{ + padding: 5px; + margin: 0; +} + +/* fix copybtn overflow problem in chromium (needed for 'sphinx_copybutton') */ +div.nbinput.container div.input_area > div[class^='highlight'], +div.nboutput.container div.output_area > div[class^='highlight']{ + overflow-y: hidden; +} + +/* hide copy button on prompts for 'sphinx_copybutton' extension ... */ +.prompt .copybtn, +/* ... and 'sphinx_immaterial' theme */ +.prompt .md-clipboard.md-icon { + display: none; +} + +/* Some additional styling taken form the Jupyter notebook CSS */ +.jp-RenderedHTMLCommon table, +div.rendered_html table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 12px; + table-layout: fixed; +} +.jp-RenderedHTMLCommon thead, +div.rendered_html thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} +.jp-RenderedHTMLCommon tr, +.jp-RenderedHTMLCommon th, +.jp-RenderedHTMLCommon td, +div.rendered_html tr, +div.rendered_html th, +div.rendered_html td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +.jp-RenderedHTMLCommon th, +div.rendered_html th { + font-weight: bold; +} +.jp-RenderedHTMLCommon tbody tr:nth-child(odd), +div.rendered_html tbody tr:nth-child(odd) { + background: #f5f5f5; +} +.jp-RenderedHTMLCommon tbody tr:hover, +div.rendered_html tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + diff --git a/_static/nbsphinx-gallery.css b/_static/nbsphinx-gallery.css new file mode 100644 index 0000000000..365c27a96b --- /dev/null +++ b/_static/nbsphinx-gallery.css @@ -0,0 +1,31 @@ +.nbsphinx-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 5px; + margin-top: 1em; + margin-bottom: 1em; +} + +.nbsphinx-gallery > a { + padding: 5px; + border: 1px dotted currentColor; + border-radius: 2px; + text-align: center; +} + +.nbsphinx-gallery > a:hover { + border-style: solid; +} + +.nbsphinx-gallery img { + max-width: 100%; + max-height: 100%; +} + +.nbsphinx-gallery > a > div:first-child { + display: flex; + align-items: start; + justify-content: center; + height: 120px; + margin-bottom: 5px; +} diff --git a/_static/nbsphinx-no-thumbnail.svg b/_static/nbsphinx-no-thumbnail.svg new file mode 100644 index 0000000000..9dca7588fa --- /dev/null +++ b/_static/nbsphinx-no-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/_static/no_image.png b/_static/no_image.png new file mode 100644 index 0000000000..8c2d48d5d3 Binary files /dev/null and b/_static/no_image.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000000..7107cec93a Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000000..997797f270 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #7971292e } +html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } +html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ +html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ +html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ +html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js new file mode 100644 index 0000000000..ef07e0bca0 --- /dev/null +++ b/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>w,afterRead:()=>b,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>G,auto:()=>r,basePlacements:()=>a,beforeMain:()=>v,beforeRead:()=>m,beforeWrite:()=>A,bottom:()=>n,clippingParents:()=>h,computeStyles:()=>et,createPopper:()=>Dt,createPopperBase:()=>Lt,createPopperLite:()=>$t,detectOverflow:()=>mt,end:()=>c,eventListeners:()=>nt,flip:()=>_t,hide:()=>yt,left:()=>o,main:()=>y,modifierPhases:()=>T,offset:()=>wt,placements:()=>g,popper:()=>u,popperGenerator:()=>kt,popperOffsets:()=>At,preventOverflow:()=>Et,read:()=>_,reference:()=>f,right:()=>s,start:()=>l,top:()=>i,variationPlacements:()=>p,viewport:()=>d,write:()=>E});var i="top",n="bottom",s="right",o="left",r="auto",a=[i,n,s,o],l="start",c="end",h="clippingParents",d="viewport",u="popper",f="reference",p=a.reduce((function(t,e){return t.concat([e+"-"+l,e+"-"+c])}),[]),g=[].concat(a,[r]).reduce((function(t,e){return t.concat([e,e+"-"+l,e+"-"+c])}),[]),m="beforeRead",_="read",b="afterRead",v="beforeMain",y="main",w="afterMain",A="beforeWrite",E="write",C="afterWrite",T=[m,_,b,v,y,w,A,E,C];function O(t){return t?(t.nodeName||"").toLowerCase():null}function x(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function k(t){return t instanceof x(t).Element||t instanceof Element}function L(t){return t instanceof x(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof x(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];L(s)&&O(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});L(n)&&O(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function S(t){return t.split("-")[0]}var I=Math.max,N=Math.min,P=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function M(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&L(t)&&(s=t.offsetWidth>0&&P(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&P(n.height)/t.offsetHeight||1);var r=(k(t)?x(t):window).visualViewport,a=!M()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function F(t){return x(t).getComputedStyle(t)}function z(t){return["table","td","th"].indexOf(O(t))>=0}function q(t){return((k(t)?t.ownerDocument:t.document)||window.document).documentElement}function R(t){return"html"===O(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function V(t){return L(t)&&"fixed"!==F(t).position?t.offsetParent:null}function Y(t){for(var e=x(t),i=V(t);i&&z(i)&&"static"===F(i).position;)i=V(i);return i&&("html"===O(i)||"body"===O(i)&&"static"===F(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&L(t)&&"fixed"===F(t).position)return null;var i=R(t);for(D(i)&&(i=i.host);L(i)&&["html","body"].indexOf(O(i))<0;){var n=F(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function K(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Q(t,e,i){return I(t,N(e,i))}function X(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function U(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const G={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,r=t.state,l=t.name,c=t.options,h=r.elements.arrow,d=r.modifiersData.popperOffsets,u=S(r.placement),f=K(u),p=[o,s].indexOf(u)>=0?"height":"width";if(h&&d){var g=function(t,e){return X("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:U(t,a))}(c.padding,r),m=B(h),_="y"===f?i:o,b="y"===f?n:s,v=r.rects.reference[p]+r.rects.reference[f]-d[f]-r.rects.popper[p],y=d[f]-r.rects.reference[f],w=Y(h),A=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,E=v/2-y/2,C=g[_],T=A-m[p]-g[b],O=A/2-m[p]/2+E,x=Q(C,O,T),k=f;r.modifiersData[l]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function J(t){return t.split("-")[1]}var Z={top:"auto",right:"auto",bottom:"auto",left:"auto"};function tt(t){var e,r=t.popper,a=t.popperRect,l=t.placement,h=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,g=t.roundOffsets,m=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof g?g({x:b,y}):{x:b,y};b=w.x,y=w.y;var A=d.hasOwnProperty("x"),E=d.hasOwnProperty("y"),C=o,T=i,O=window;if(p){var k=Y(r),L="clientHeight",D="clientWidth";k===x(r)&&"static"!==F(k=q(r)).position&&"absolute"===u&&(L="scrollHeight",D="scrollWidth"),(l===i||(l===o||l===s)&&h===c)&&(T=n,y-=(m&&k===O&&O.visualViewport?O.visualViewport.height:k[L])-a.height,y*=f?1:-1),l!==o&&(l!==i&&l!==n||h!==c)||(C=s,b-=(m&&k===O&&O.visualViewport?O.visualViewport.width:k[D])-a.width,b*=f?1:-1)}var $,S=Object.assign({position:u},p&&Z),I=!0===g?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:P(i*s)/s||0,y:P(n*s)/s||0}}({x:b,y},x(r)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},S,(($={})[T]=E?"0":"",$[C]=A?"0":"",$.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",$)):Object.assign({},S,((e={})[T]=E?y+"px":"",e[C]=A?b+"px":"",e.transform="",e))}const et={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:S(e.placement),variation:J(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,tt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,tt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var it={passive:!0};const nt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=x(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,it)})),a&&l.addEventListener("resize",i.update,it),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,it)})),a&&l.removeEventListener("resize",i.update,it)}},data:{}};var st={left:"right",right:"left",bottom:"top",top:"bottom"};function ot(t){return t.replace(/left|right|bottom|top/g,(function(t){return st[t]}))}var rt={start:"end",end:"start"};function at(t){return t.replace(/start|end/g,(function(t){return rt[t]}))}function lt(t){var e=x(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ct(t){return H(q(t)).left+lt(t).scrollLeft}function ht(t){var e=F(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function dt(t){return["html","body","#document"].indexOf(O(t))>=0?t.ownerDocument.body:L(t)&&ht(t)?t:dt(R(t))}function ut(t,e){var i;void 0===e&&(e=[]);var n=dt(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=x(n),r=s?[o].concat(o.visualViewport||[],ht(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ut(R(r)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function pt(t,e,i){return e===d?ft(function(t,e){var i=x(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=M();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ct(t),y:l}}(t,i)):k(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=q(t),n=lt(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=I(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=I(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ct(t),l=-n.scrollTop;return"rtl"===F(s||i).direction&&(a+=I(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,r=t.reference,a=t.element,h=t.placement,d=h?S(h):null,u=h?J(h):null,f=r.x+r.width/2-a.width/2,p=r.y+r.height/2-a.height/2;switch(d){case i:e={x:f,y:r.y-a.height};break;case n:e={x:f,y:r.y+r.height};break;case s:e={x:r.x+r.width,y:p};break;case o:e={x:r.x-a.width,y:p};break;default:e={x:r.x,y:r.y}}var g=d?K(d):null;if(null!=g){var m="y"===g?"height":"width";switch(u){case l:e[g]=e[g]-(r[m]/2-a[m]/2);break;case c:e[g]=e[g]+(r[m]/2-a[m]/2)}}return e}function mt(t,e){void 0===e&&(e={});var o=e,r=o.placement,l=void 0===r?t.placement:r,c=o.strategy,p=void 0===c?t.strategy:c,g=o.boundary,m=void 0===g?h:g,_=o.rootBoundary,b=void 0===_?d:_,v=o.elementContext,y=void 0===v?u:v,w=o.altBoundary,A=void 0!==w&&w,E=o.padding,C=void 0===E?0:E,T=X("number"!=typeof C?C:U(C,a)),x=y===u?f:u,D=t.rects.popper,$=t.elements[A?x:y],S=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ut(R(t)),i=["absolute","fixed"].indexOf(F(t).position)>=0&&L(t)?Y(t):t;return k(i)?e.filter((function(t){return k(t)&&W(t,i)&&"body"!==O(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=pt(t,i,n);return e.top=I(s.top,e.top),e.right=N(s.right,e.right),e.bottom=N(s.bottom,e.bottom),e.left=I(s.left,e.left),e}),pt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(k($)?$:$.contextElement||q(t.elements.popper),m,b,p),P=H(t.elements.reference),j=gt({reference:P,element:D,strategy:"absolute",placement:l}),M=ft(Object.assign({},D,j)),B=y===u?M:P,z={top:S.top-B.top+T.top,bottom:B.bottom-S.bottom+T.bottom,left:S.left-B.left+T.left,right:B.right-S.right+T.right},V=t.modifiersData.offset;if(y===u&&V){var K=V[l];Object.keys(z).forEach((function(t){var e=[s,n].indexOf(t)>=0?1:-1,o=[i,n].indexOf(t)>=0?"y":"x";z[t]+=K[o]*e}))}return z}const _t={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,c=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=c.mainAxis,u=void 0===d||d,f=c.altAxis,m=void 0===f||f,_=c.fallbackPlacements,b=c.padding,v=c.boundary,y=c.rootBoundary,w=c.altBoundary,A=c.flipVariations,E=void 0===A||A,C=c.allowedAutoPlacements,T=e.options.placement,O=S(T),x=_||(O!==T&&E?function(t){if(S(t)===r)return[];var e=ot(t);return[at(t),e,at(e)]}(T):[ot(T)]),k=[T].concat(x).reduce((function(t,i){return t.concat(S(i)===r?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,l=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=J(n),u=d?l?p:p.filter((function(t){return J(t)===d})):a,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var m=f.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[S(i)],e}),{});return Object.keys(m).sort((function(t,e){return m[t]-m[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:E,allowedAutoPlacements:C}):i)}),[]),L=e.rects.reference,D=e.rects.popper,$=new Map,I=!0,N=k[0],P=0;P=0,W=B?"width":"height",F=mt(e,{placement:j,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=B?H?s:o:H?n:i;L[W]>D[W]&&(z=ot(z));var q=ot(z),R=[];if(u&&R.push(F[M]<=0),m&&R.push(F[z]<=0,F[q]<=0),R.every((function(t){return t}))){N=j,I=!1;break}$.set(j,R)}if(I)for(var V=function(t){var e=k.find((function(e){var i=$.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=E?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function bt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function vt(t){return[i,s,n,o].some((function(e){return t[e]>=0}))}const yt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=mt(e,{elementContext:"reference"}),a=mt(e,{altBoundary:!0}),l=bt(r,n),c=bt(a,s,o),h=vt(l),d=vt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},wt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,n=t.options,r=t.name,a=n.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,n){return t[n]=function(t,e,n){var r=S(t),a=[o,i].indexOf(r)>=0?-1:1,l="function"==typeof n?n(Object.assign({},e,{placement:t})):n,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[o,s].indexOf(r)>=0?{x:h,y:c}:{x:c,y:h}}(n,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[r]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Et={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,r=t.options,a=t.name,c=r.mainAxis,h=void 0===c||c,d=r.altAxis,u=void 0!==d&&d,f=r.boundary,p=r.rootBoundary,g=r.altBoundary,m=r.padding,_=r.tether,b=void 0===_||_,v=r.tetherOffset,y=void 0===v?0:v,w=mt(e,{boundary:f,rootBoundary:p,padding:m,altBoundary:g}),A=S(e.placement),E=J(e.placement),C=!E,T=K(A),O="x"===T?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,D="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,$="number"==typeof D?{mainAxis:D,altAxis:D}:Object.assign({mainAxis:0,altAxis:0},D),P=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,j={x:0,y:0};if(x){if(h){var M,H="y"===T?i:o,W="y"===T?n:s,F="y"===T?"height":"width",z=x[T],q=z+w[H],R=z-w[W],V=b?-L[F]/2:0,X=E===l?k[F]:L[F],U=E===l?-L[F]:-k[F],G=e.elements.arrow,Z=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[H],it=tt[W],nt=Q(0,k[F],Z[F]),st=C?k[F]/2-V-nt-et-$.mainAxis:X-nt-et-$.mainAxis,ot=C?-k[F]/2+V+nt+it+$.mainAxis:U+nt+it+$.mainAxis,rt=e.elements.arrow&&Y(e.elements.arrow),at=rt?"y"===T?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(M=null==P?void 0:P[T])?M:0,ct=z+ot-lt,ht=Q(b?N(q,z+st-lt-at):q,z,b?I(R,ct):R);x[T]=ht,j[T]=ht-z}if(u){var dt,ut="x"===T?i:o,ft="x"===T?n:s,pt=x[O],gt="y"===O?"height":"width",_t=pt+w[ut],bt=pt-w[ft],vt=-1!==[i,o].indexOf(A),yt=null!=(dt=null==P?void 0:P[O])?dt:0,wt=vt?_t:pt-k[gt]-L[gt]-yt+$.altAxis,At=vt?pt+k[gt]+L[gt]-yt-$.altAxis:bt,Et=b&&vt?function(t,e,i){var n=Q(t,e,i);return n>i?i:n}(wt,pt,At):Q(b?wt:_t,pt,b?At:bt);x[O]=Et,j[O]=Et-pt}e.modifiersData[a]=j}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=L(e),r=L(e)&&function(t){var e=t.getBoundingClientRect(),i=P(e.width)/t.offsetWidth||1,n=P(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==O(e)||ht(a))&&(c=(n=e)!==x(n)&&L(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:lt(n)),L(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ct(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Tt(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Ot={placement:"bottom",modifiers:[],strategy:"absolute"};function xt(){for(var t=arguments.length,e=new Array(t),i=0;i{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},Nt=t=>{const e=It(t);return e&&document.querySelector(e)?e:null},Pt=t=>{const e=It(t);return e?document.querySelector(e):null},jt=t=>{t.dispatchEvent(new Event(St))},Mt=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Mt(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,Bt=t=>{if(!Mt(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),Ft=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?Ft(t.parentNode):null},zt=()=>{},qt=t=>{t.offsetHeight},Rt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Vt=[],Yt=()=>"rtl"===document.documentElement.dir,Kt=t=>{var e;e=()=>{const e=Rt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Vt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Vt)t()})),Vt.push(e)):e()},Qt=t=>{"function"==typeof t&&t()},Xt=(t,e,i=!0)=>{if(!i)return void Qt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(St,o),Qt(t))};e.addEventListener(St,o),setTimeout((()=>{s||jt(e)}),n)},Ut=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Gt=/[^.]*(?=\..*)\.|.*/,Jt=/\..*/,Zt=/::\d+$/,te={};let ee=1;const ie={mouseenter:"mouseover",mouseleave:"mouseout"},ne=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function se(t,e){return e&&`${e}::${ee++}`||t.uidEvent||ee++}function oe(t){const e=se(t);return t.uidEvent=e,te[e]=te[e]||{},te[e]}function re(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function ae(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=de(t);return ne.has(o)||(o=t),[n,s,o]}function le(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=ae(e,i,n);if(e in ie){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=oe(t),c=l[a]||(l[a]={}),h=re(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=se(r,e.replace(Gt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return fe(s,{delegateTarget:r}),n.oneOff&&ue.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return fe(n,{delegateTarget:t}),i.oneOff&&ue.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function ce(t,e,i,n,s){const o=re(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function he(t,e,i,n){const s=e[i]||{};for(const o of Object.keys(s))if(o.includes(n)){const n=s[o];ce(t,e,i,n.callable,n.delegationSelector)}}function de(t){return t=t.replace(Jt,""),ie[t]||t}const ue={on(t,e,i,n){le(t,e,i,n,!1)},one(t,e,i,n){le(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=ae(e,i,n),a=r!==e,l=oe(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))he(t,l,i,e.slice(1));for(const i of Object.keys(c)){const n=i.replace(Zt,"");if(!a||e.includes(n)){const e=c[i];ce(t,l,r,e.callable,e.delegationSelector)}}}else{if(!Object.keys(c).length)return;ce(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Rt();let s=null,o=!0,r=!0,a=!1;e!==de(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());let l=new Event(e,{bubbles:o,cancelable:!0});return l=fe(l,i),a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function fe(t,e){for(const[i,n]of Object.entries(e||{}))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}const pe=new Map,ge={set(t,e,i){pe.has(t)||pe.set(t,new Map);const n=pe.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>pe.has(t)&&pe.get(t).get(e)||null,remove(t,e){if(!pe.has(t))return;const i=pe.get(t);i.delete(e),0===i.size&&pe.delete(t)}};function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function _e(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const be={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${_e(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${_e(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${_e(e)}`))};class ve{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Mt(e)?be.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Mt(e)?be.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const n of Object.keys(e)){const s=e[n],o=t[n],r=Mt(o)?"element":null==(i=o)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class ye extends ve{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),ge.set(this._element,this.constructor.DATA_KEY,this))}dispose(){ge.remove(this._element,this.constructor.DATA_KEY),ue.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Xt(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return ge.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.2.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const we=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;ue.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=Pt(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Ee=`close${Ae}`,Ce=`closed${Ae}`;class Te extends ye{static get NAME(){return"alert"}close(){if(ue.trigger(this._element,Ee).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),ue.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Te.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}we(Te,"close"),Kt(Te);const Oe='[data-bs-toggle="button"]';class xe extends ye{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=xe.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}ue.on(document,"click.bs.button.data-api",Oe,(t=>{t.preventDefault();const e=t.target.closest(Oe);xe.getOrCreateInstance(e).toggle()})),Kt(xe);const ke={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))}},Le=".bs.swipe",De=`touchstart${Le}`,$e=`touchmove${Le}`,Se=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},je={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Me extends ve{constructor(t,e){super(),this._element=t,t&&Me.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return je}static get NAME(){return"swipe"}dispose(){ue.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Qt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Qt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(ue.on(this._element,Ie,(t=>this._start(t))),ue.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(ue.on(this._element,De,(t=>this._start(t))),ue.on(this._element,$e,(t=>this._move(t))),ue.on(this._element,Se,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const He=".bs.carousel",Be=".data-api",We="next",Fe="prev",ze="left",qe="right",Re=`slide${He}`,Ve=`slid${He}`,Ye=`keydown${He}`,Ke=`mouseenter${He}`,Qe=`mouseleave${He}`,Xe=`dragstart${He}`,Ue=`load${He}${Be}`,Ge=`click${He}${Be}`,Je="carousel",Ze="active",ti=".active",ei=".carousel-item",ii=ti+ei,ni={ArrowLeft:qe,ArrowRight:ze},si={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},oi={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ri extends ye{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=ke.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Je&&this.cycle()}static get Default(){return si}static get DefaultType(){return oi}static get NAME(){return"carousel"}next(){this._slide(We)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(Fe)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?ue.one(this._element,Ve,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void ue.one(this._element,Ve,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?We:Fe;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&ue.on(this._element,Ye,(t=>this._keydown(t))),"hover"===this._config.pause&&(ue.on(this._element,Ke,(()=>this.pause())),ue.on(this._element,Qe,(()=>this._maybeEnableCycle()))),this._config.touch&&Me.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of ke.find(".carousel-item img",this._element))ue.on(t,Xe,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ze)),rightCallback:()=>this._slide(this._directionToOrder(qe)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new Me(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=ni[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=ke.findOne(ti,this._indicatorsElement);e.classList.remove(Ze),e.removeAttribute("aria-current");const i=ke.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Ze),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===We,s=e||Ut(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>ue.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Re).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(Ze),i.classList.remove(Ze,c,l),this._isSliding=!1,r(Ve)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return ke.findOne(ii,this._element)}_getItems(){return ke.find(ei,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Yt()?t===ze?Fe:We:t===ze?We:Fe}_orderToDirection(t){return Yt()?t===Fe?ze:qe:t===Fe?qe:ze}static jQueryInterface(t){return this.each((function(){const e=ri.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}ue.on(document,Ge,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=Pt(this);if(!e||!e.classList.contains(Je))return;t.preventDefault();const i=ri.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===be.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),ue.on(window,Ue,(()=>{const t=ke.find('[data-bs-ride="carousel"]');for(const e of t)ri.getOrCreateInstance(e)})),Kt(ri);const ai=".bs.collapse",li=`show${ai}`,ci=`shown${ai}`,hi=`hide${ai}`,di=`hidden${ai}`,ui=`click${ai}.data-api`,fi="show",pi="collapse",gi="collapsing",mi=`:scope .${pi} .${pi}`,_i='[data-bs-toggle="collapse"]',bi={parent:null,toggle:!0},vi={parent:"(null|element)",toggle:"boolean"};class yi extends ye{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=ke.find(_i);for(const t of i){const e=Nt(t),i=ke.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return bi}static get DefaultType(){return vi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>yi.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(ue.trigger(this._element,li).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(pi),this._element.classList.add(gi),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(gi),this._element.classList.add(pi,fi),this._element.style[e]="",ue.trigger(this._element,ci)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(ue.trigger(this._element,hi).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(gi),this._element.classList.remove(pi,fi);for(const t of this._triggerArray){const e=Pt(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(gi),this._element.classList.add(pi),ue.trigger(this._element,di)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(fi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(_i);for(const e of t){const t=Pt(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=ke.find(mi,this._config.parent);return ke.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=yi.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}ue.on(document,ui,_i,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=Nt(this),i=ke.find(e);for(const t of i)yi.getOrCreateInstance(t,{toggle:!1}).toggle()})),Kt(yi);const wi="dropdown",Ai=".bs.dropdown",Ei=".data-api",Ci="ArrowUp",Ti="ArrowDown",Oi=`hide${Ai}`,xi=`hidden${Ai}`,ki=`show${Ai}`,Li=`shown${Ai}`,Di=`click${Ai}${Ei}`,$i=`keydown${Ai}${Ei}`,Si=`keyup${Ai}${Ei}`,Ii="show",Ni='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Pi=`${Ni}.${Ii}`,ji=".dropdown-menu",Mi=Yt()?"top-end":"top-start",Hi=Yt()?"top-start":"top-end",Bi=Yt()?"bottom-end":"bottom-start",Wi=Yt()?"bottom-start":"bottom-end",Fi=Yt()?"left-start":"right-start",zi=Yt()?"right-start":"left-start",qi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Ri={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Vi extends ye{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=ke.next(this._element,ji)[0]||ke.prev(this._element,ji)[0]||ke.findOne(ji,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return qi}static get DefaultType(){return Ri}static get NAME(){return wi}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!ue.trigger(this._element,ki,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))ue.on(t,"mouseover",zt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Ii),this._element.classList.add(Ii),ue.trigger(this._element,Li,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!ue.trigger(this._element,Oi,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ue.off(t,"mouseover",zt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Ii),this._element.classList.remove(Ii),this._element.setAttribute("aria-expanded","false"),be.removeDataAttribute(this._menu,"popper"),ue.trigger(this._element,xi,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Mt(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${wi.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Mt(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Ii)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Fi;if(t.classList.contains("dropstart"))return zi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Hi:Mi:e?Wi:Bi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(be.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=ke.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Ut(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Vi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=ke.find(Pi);for(const i of e){const e=Vi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ci,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ni)?this:ke.prev(this,Ni)[0]||ke.next(this,Ni)[0]||ke.findOne(Ni,t.delegateTarget.parentNode),o=Vi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}ue.on(document,$i,Ni,Vi.dataApiKeydownHandler),ue.on(document,$i,ji,Vi.dataApiKeydownHandler),ue.on(document,Di,Vi.clearMenus),ue.on(document,Si,Vi.clearMenus),ue.on(document,Di,Ni,(function(t){t.preventDefault(),Vi.getOrCreateInstance(this).toggle()})),Kt(Vi);const Yi=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ki=".sticky-top",Qi="padding-right",Xi="margin-right";class Ui{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Qi,(e=>e+t)),this._setElementAttributes(Yi,Qi,(e=>e+t)),this._setElementAttributes(Ki,Xi,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Qi),this._resetElementAttributes(Yi,Qi),this._resetElementAttributes(Ki,Xi)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&be.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=be.getDataAttribute(t,e);null!==i?(be.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Mt(t))e(t);else for(const i of ke.find(t,this._element))e(i)}}const Gi="backdrop",Ji="show",Zi=`mousedown.bs.${Gi}`,tn={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},en={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class nn extends ve{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return tn}static get DefaultType(){return en}static get NAME(){return Gi}show(t){if(!this._config.isVisible)return void Qt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Ji),this._emulateAnimation((()=>{Qt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ji),this._emulateAnimation((()=>{this.dispose(),Qt(t)}))):Qt(t)}dispose(){this._isAppended&&(ue.off(this._element,Zi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),ue.on(t,Zi,(()=>{Qt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Xt(t,this._getElement(),this._config.isAnimated)}}const sn=".bs.focustrap",on=`focusin${sn}`,rn=`keydown.tab${sn}`,an="backward",ln={autofocus:!0,trapElement:null},cn={autofocus:"boolean",trapElement:"element"};class hn extends ve{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return ln}static get DefaultType(){return cn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),ue.off(document,sn),ue.on(document,on,(t=>this._handleFocusin(t))),ue.on(document,rn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,ue.off(document,sn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=ke.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===an?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?an:"forward")}}const dn=".bs.modal",un=`hide${dn}`,fn=`hidePrevented${dn}`,pn=`hidden${dn}`,gn=`show${dn}`,mn=`shown${dn}`,_n=`resize${dn}`,bn=`click.dismiss${dn}`,vn=`mousedown.dismiss${dn}`,yn=`keydown.dismiss${dn}`,wn=`click${dn}.data-api`,An="modal-open",En="show",Cn="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},On={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class xn extends ye{constructor(t,e){super(t,e),this._dialog=ke.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Ui,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return On}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||ue.trigger(this._element,gn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(An),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(ue.trigger(this._element,un).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(En),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])ue.off(t,dn);this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new nn({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new hn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=ke.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(En),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,ue.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){ue.on(this._element,yn,(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),ue.on(window,_n,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),ue.on(this._element,vn,(t=>{ue.one(this._element,bn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(An),this._resetAdjustments(),this._scrollBar.reset(),ue.trigger(this._element,pn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(ue.trigger(this._element,fn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Cn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Cn),this._queueCallback((()=>{this._element.classList.remove(Cn),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Yt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Yt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=xn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}ue.on(document,wn,'[data-bs-toggle="modal"]',(function(t){const e=Pt(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),ue.one(e,gn,(t=>{t.defaultPrevented||ue.one(e,pn,(()=>{Bt(this)&&this.focus()}))}));const i=ke.findOne(".modal.show");i&&xn.getInstance(i).hide(),xn.getOrCreateInstance(e).toggle(this)})),we(xn),Kt(xn);const kn=".bs.offcanvas",Ln=".data-api",Dn=`load${kn}${Ln}`,$n="show",Sn="showing",In="hiding",Nn=".offcanvas.show",Pn=`show${kn}`,jn=`shown${kn}`,Mn=`hide${kn}`,Hn=`hidePrevented${kn}`,Bn=`hidden${kn}`,Wn=`resize${kn}`,Fn=`click${kn}${Ln}`,zn=`keydown.dismiss${kn}`,qn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Vn extends ye{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return qn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||ue.trigger(this._element,Pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Ui).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Sn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add($n),this._element.classList.remove(Sn),ue.trigger(this._element,jn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(ue.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(In),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove($n,In),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Ui).reset(),ue.trigger(this._element,Bn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new nn({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():ue.trigger(this._element,Hn)}:null})}_initializeFocusTrap(){return new hn({trapElement:this._element})}_addEventListeners(){ue.on(this._element,zn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():ue.trigger(this._element,Hn))}))}static jQueryInterface(t){return this.each((function(){const e=Vn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}ue.on(document,Fn,'[data-bs-toggle="offcanvas"]',(function(t){const e=Pt(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;ue.one(e,Bn,(()=>{Bt(this)&&this.focus()}));const i=ke.findOne(Nn);i&&i!==e&&Vn.getInstance(i).hide(),Vn.getOrCreateInstance(e).toggle(this)})),ue.on(window,Dn,(()=>{for(const t of ke.find(Nn))Vn.getOrCreateInstance(t).show()})),ue.on(window,Wn,(()=>{for(const t of ke.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Vn.getOrCreateInstance(t).hide()})),we(Vn),Kt(Vn);const Yn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Kn=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Qn=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Yn.has(i)||Boolean(Kn.test(t.nodeValue)||Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Un={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Gn={allowList:Un,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Jn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Zn={entry:"(string|element|function|null)",selector:"(string|element)"};class ts extends ve{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Gn}static get DefaultType(){return Jn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Zn)}_setContent(t,e,i){const n=ke.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Mt(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return"function"==typeof t?t(this):t}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const es=new Set(["sanitize","allowList","sanitizeFn"]),is="fade",ns="show",ss=".modal",os="hide.bs.modal",rs="hover",as="focus",ls={AUTO:"auto",TOP:"top",RIGHT:Yt()?"left":"right",BOTTOM:"bottom",LEFT:Yt()?"right":"left"},cs={allowList:Un,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},hs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ds extends ye{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return cs}static get DefaultType(){return hs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),ue.off(this._element.closest(ss),os,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=ue.trigger(this._element,this.constructor.eventName("show")),e=(Ft(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),ue.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ue.on(t,"mouseover",zt);this._queueCallback((()=>{ue.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!ue.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ue.off(t,"mouseover",zt);this._activeTrigger.click=!1,this._activeTrigger[as]=!1,this._activeTrigger[rs]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),ue.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(is,ns),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(is),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new ts({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(is)}_isShown(){return this.tip&&this.tip.classList.contains(ns)}_createPopper(t){const e="function"==typeof this._config.placement?this._config.placement.call(this,t,this._element):this._config.placement,i=ls[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)ue.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===rs?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===rs?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");ue.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?as:rs]=!0,e._enter()})),ue.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?as:rs]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},ue.on(this._element.closest(ss),os,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=be.getDataAttributes(this._element);for(const t of Object.keys(e))es.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=ds.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Kt(ds);const us={...ds.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},fs={...ds.DefaultType,content:"(null|string|element|function)"};class ps extends ds{static get Default(){return us}static get DefaultType(){return fs}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=ps.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Kt(ps);const gs=".bs.scrollspy",ms=`activate${gs}`,_s=`click${gs}`,bs=`load${gs}.data-api`,vs="active",ys="[href]",ws=".nav-link",As=`${ws}, .nav-item > ${ws}, .list-group-item`,Es={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Cs={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ts extends ye{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Es}static get DefaultType(){return Cs}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(ue.off(this._config.target,_s),ue.on(this._config.target,_s,ys,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=ke.find(ys,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=ke.findOne(e.hash,this._element);Bt(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(vs),this._activateParents(t),ue.trigger(this._element,ms,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))ke.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(vs);else for(const e of ke.parents(t,".nav, .list-group"))for(const t of ke.prev(e,As))t.classList.add(vs)}_clearActiveClass(t){t.classList.remove(vs);const e=ke.find(`${ys}.${vs}`,t);for(const t of e)t.classList.remove(vs)}static jQueryInterface(t){return this.each((function(){const e=Ts.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}ue.on(window,bs,(()=>{for(const t of ke.find('[data-bs-spy="scroll"]'))Ts.getOrCreateInstance(t)})),Kt(Ts);const Os=".bs.tab",xs=`hide${Os}`,ks=`hidden${Os}`,Ls=`show${Os}`,Ds=`shown${Os}`,$s=`click${Os}`,Ss=`keydown${Os}`,Is=`load${Os}`,Ns="ArrowLeft",Ps="ArrowRight",js="ArrowUp",Ms="ArrowDown",Hs="active",Bs="fade",Ws="show",Fs=":not(.dropdown-toggle)",zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${Fs}, .list-group-item${Fs}, [role="tab"]${Fs}, ${zs}`,Rs=`.${Hs}[data-bs-toggle="tab"], .${Hs}[data-bs-toggle="pill"], .${Hs}[data-bs-toggle="list"]`;class Vs extends ye{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),ue.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?ue.trigger(e,xs,{relatedTarget:t}):null;ue.trigger(t,Ls,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Hs),this._activate(Pt(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),ue.trigger(t,Ds,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Bs)))}_deactivate(t,e){t&&(t.classList.remove(Hs),t.blur(),this._deactivate(Pt(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),ue.trigger(t,ks,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Bs)))}_keydown(t){if(![Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[Ps,Ms].includes(t.key),i=Ut(this._getChildren().filter((t=>!Wt(t))),t.target,e,!0);i&&(i.focus({preventScroll:!0}),Vs.getOrCreateInstance(i).show())}_getChildren(){return ke.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=Pt(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=ke.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",Hs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Hs)}_getInnerElement(t){return t.matches(qs)?t:ke.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Vs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}ue.on(document,$s,zs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Vs.getOrCreateInstance(this).show()})),ue.on(window,Is,(()=>{for(const t of ke.find(Rs))Vs.getOrCreateInstance(t)})),Kt(Vs);const Ys=".bs.toast",Ks=`mouseover${Ys}`,Qs=`mouseout${Ys}`,Xs=`focusin${Ys}`,Us=`focusout${Ys}`,Gs=`hide${Ys}`,Js=`hidden${Ys}`,Zs=`show${Ys}`,to=`shown${Ys}`,eo="hide",io="show",no="showing",so={animation:"boolean",autohide:"boolean",delay:"number"},oo={animation:!0,autohide:!0,delay:5e3};class ro extends ye{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return oo}static get DefaultType(){return so}static get NAME(){return"toast"}show(){ue.trigger(this._element,Zs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(eo),qt(this._element),this._element.classList.add(io,no),this._queueCallback((()=>{this._element.classList.remove(no),ue.trigger(this._element,to),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(ue.trigger(this._element,Gs).defaultPrevented||(this._element.classList.add(no),this._queueCallback((()=>{this._element.classList.add(eo),this._element.classList.remove(no,io),ue.trigger(this._element,Js)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(io),super.dispose()}isShown(){return this._element.classList.contains(io)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){ue.on(this._element,Ks,(t=>this._onInteraction(t,!0))),ue.on(this._element,Qs,(t=>this._onInteraction(t,!1))),ue.on(this._element,Xs,(t=>this._onInteraction(t,!0))),ue.on(this._element,Us,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ro.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function ao(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}we(ro),Kt(ro),ao((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new ds(t,{delay:{show:500,hide:100}})}))})),ao((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),ao((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))}))})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 0000000000..91ad10aa07 --- /dev/null +++ b/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.2.3 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map new file mode 100644 index 0000000000..04c27d7bcd --- /dev/null +++ b/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":";mBACA,IAAIA,EAAsB,CCA1BA,EAAwB,CAACC,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXF,EAAoBI,EAAEF,EAAYC,KAASH,EAAoBI,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDH,EAAwB,CAACS,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFV,EAAyBC,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,ipBCLvD,IAAI,EAAM,MACNC,EAAS,SACTC,EAAQ,QACRC,EAAO,OACPC,EAAO,OACPC,EAAiB,CAAC,EAAKJ,EAAQC,EAAOC,GACtCG,EAAQ,QACRC,EAAM,MACNC,EAAkB,kBAClBC,EAAW,WACXC,EAAS,SACTC,EAAY,YACZC,EAAmCP,EAAeQ,QAAO,SAAUC,EAAKC,GACjF,OAAOD,EAAIE,OAAO,CAACD,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAChE,GAAG,IACQ,EAA0B,GAAGS,OAAOX,EAAgB,CAACD,IAAOS,QAAO,SAAUC,EAAKC,GAC3F,OAAOD,EAAIE,OAAO,CAACD,EAAWA,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAC3E,GAAG,IAEQU,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAc,cACdC,EAAQ,QACRC,EAAa,aACbC,EAAiB,CAACT,EAAYC,EAAMC,EAAWC,EAAYC,EAAMC,EAAWC,EAAaC,EAAOC,GC9B5F,SAASE,EAAYC,GAClC,OAAOA,GAAWA,EAAQC,UAAY,IAAIC,cAAgB,IAC5D,CCFe,SAASC,EAAUC,GAChC,GAAY,MAARA,EACF,OAAOC,OAGT,GAAwB,oBAApBD,EAAKE,WAAkC,CACzC,IAAIC,EAAgBH,EAAKG,cACzB,OAAOA,GAAgBA,EAAcC,aAAwBH,MAC/D,CAEA,OAAOD,CACT,CCTA,SAASK,EAAUL,GAEjB,OAAOA,aADUD,EAAUC,GAAMM,SACIN,aAAgBM,OACvD,CAEA,SAASC,EAAcP,GAErB,OAAOA,aADUD,EAAUC,GAAMQ,aACIR,aAAgBQ,WACvD,CAEA,SAASC,EAAaT,GAEpB,MAA0B,oBAAfU,aAKJV,aADUD,EAAUC,GAAMU,YACIV,aAAgBU,WACvD,CCwDA,SACEC,KAAM,cACNC,SAAS,EACTC,MAAO,QACPC,GA5EF,SAAqBC,GACnB,IAAIC,EAAQD,EAAKC,MACjB3D,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIS,EAAQJ,EAAMK,OAAOV,IAAS,CAAC,EAC/BW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EACxCf,EAAUoB,EAAME,SAASP,GAExBJ,EAAcX,IAAaD,EAAYC,KAO5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUR,GACxC,IAAI3C,EAAQsD,EAAWX,IAET,IAAV3C,EACF4B,EAAQ4B,gBAAgBb,GAExBf,EAAQ6B,aAAad,GAAgB,IAAV3C,EAAiB,GAAKA,EAErD,IACF,GACF,EAoDE0D,OAlDF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MACdY,EAAgB,CAClBlD,OAAQ,CACNmD,SAAUb,EAAMc,QAAQC,SACxB5D,KAAM,IACN6D,IAAK,IACLC,OAAQ,KAEVC,MAAO,CACLL,SAAU,YAEZlD,UAAW,CAAC,GASd,OAPAtB,OAAOkE,OAAOP,EAAME,SAASxC,OAAO0C,MAAOQ,EAAclD,QACzDsC,EAAMK,OAASO,EAEXZ,EAAME,SAASgB,OACjB7E,OAAOkE,OAAOP,EAAME,SAASgB,MAAMd,MAAOQ,EAAcM,OAGnD,WACL7E,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIf,EAAUoB,EAAME,SAASP,GACzBW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EAGxCS,EAFkB/D,OAAO4D,KAAKD,EAAMK,OAAOzD,eAAe+C,GAAQK,EAAMK,OAAOV,GAAQiB,EAAcjB,IAE7E9B,QAAO,SAAUuC,EAAOe,GAElD,OADAf,EAAMe,GAAY,GACXf,CACT,GAAG,CAAC,GAECb,EAAcX,IAAaD,EAAYC,KAI5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUiB,GACxCxC,EAAQ4B,gBAAgBY,EAC1B,IACF,GACF,CACF,EASEC,SAAU,CAAC,kBCjFE,SAASC,EAAiBvD,GACvC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCHO,IAAI,EAAMC,KAAKC,IACX,EAAMD,KAAKE,IACXC,EAAQH,KAAKG,MCFT,SAASC,IACtB,IAAIC,EAASC,UAAUC,cAEvB,OAAc,MAAVF,GAAkBA,EAAOG,QAAUC,MAAMC,QAAQL,EAAOG,QACnDH,EAAOG,OAAOG,KAAI,SAAUC,GACjC,OAAOA,EAAKC,MAAQ,IAAMD,EAAKE,OACjC,IAAGC,KAAK,KAGHT,UAAUU,SACnB,CCTe,SAASC,IACtB,OAAQ,iCAAiCC,KAAKd,IAChD,CCCe,SAASe,EAAsB/D,EAASgE,EAAcC,QAC9C,IAAjBD,IACFA,GAAe,QAGO,IAApBC,IACFA,GAAkB,GAGpB,IAAIC,EAAalE,EAAQ+D,wBACrBI,EAAS,EACTC,EAAS,EAETJ,GAAgBrD,EAAcX,KAChCmE,EAASnE,EAAQqE,YAAc,GAAItB,EAAMmB,EAAWI,OAAStE,EAAQqE,aAAmB,EACxFD,EAASpE,EAAQuE,aAAe,GAAIxB,EAAMmB,EAAWM,QAAUxE,EAAQuE,cAAoB,GAG7F,IACIE,GADOhE,EAAUT,GAAWG,EAAUH,GAAWK,QAC3BoE,eAEtBC,GAAoBb,KAAsBI,EAC1CU,GAAKT,EAAW3F,MAAQmG,GAAoBD,EAAiBA,EAAeG,WAAa,IAAMT,EAC/FU,GAAKX,EAAW9B,KAAOsC,GAAoBD,EAAiBA,EAAeK,UAAY,IAAMV,EAC7FE,EAAQJ,EAAWI,MAAQH,EAC3BK,EAASN,EAAWM,OAASJ,EACjC,MAAO,CACLE,MAAOA,EACPE,OAAQA,EACRpC,IAAKyC,EACLvG,MAAOqG,EAAIL,EACXjG,OAAQwG,EAAIL,EACZjG,KAAMoG,EACNA,EAAGA,EACHE,EAAGA,EAEP,CCrCe,SAASE,EAAc/E,GACpC,IAAIkE,EAAaH,EAAsB/D,GAGnCsE,EAAQtE,EAAQqE,YAChBG,EAASxE,EAAQuE,aAUrB,OARI3B,KAAKoC,IAAId,EAAWI,MAAQA,IAAU,IACxCA,EAAQJ,EAAWI,OAGjB1B,KAAKoC,IAAId,EAAWM,OAASA,IAAW,IAC1CA,EAASN,EAAWM,QAGf,CACLG,EAAG3E,EAAQ4E,WACXC,EAAG7E,EAAQ8E,UACXR,MAAOA,EACPE,OAAQA,EAEZ,CCvBe,SAASS,EAASC,EAAQC,GACvC,IAAIC,EAAWD,EAAME,aAAeF,EAAME,cAE1C,GAAIH,EAAOD,SAASE,GAClB,OAAO,EAEJ,GAAIC,GAAYvE,EAAauE,GAAW,CACzC,IAAIE,EAAOH,EAEX,EAAG,CACD,GAAIG,GAAQJ,EAAOK,WAAWD,GAC5B,OAAO,EAITA,EAAOA,EAAKE,YAAcF,EAAKG,IACjC,OAASH,EACX,CAGF,OAAO,CACT,CCrBe,SAAS,EAAiBtF,GACvC,OAAOG,EAAUH,GAAS0F,iBAAiB1F,EAC7C,CCFe,SAAS2F,EAAe3F,GACrC,MAAO,CAAC,QAAS,KAAM,MAAM4F,QAAQ7F,EAAYC,KAAa,CAChE,CCFe,SAAS6F,EAAmB7F,GAEzC,QAASS,EAAUT,GAAWA,EAAQO,cACtCP,EAAQ8F,WAAazF,OAAOyF,UAAUC,eACxC,CCFe,SAASC,EAAchG,GACpC,MAA6B,SAAzBD,EAAYC,GACPA,EAMPA,EAAQiG,cACRjG,EAAQwF,aACR3E,EAAab,GAAWA,EAAQyF,KAAO,OAEvCI,EAAmB7F,EAGvB,CCVA,SAASkG,EAAoBlG,GAC3B,OAAKW,EAAcX,IACoB,UAAvC,EAAiBA,GAASiC,SAInBjC,EAAQmG,aAHN,IAIX,CAwCe,SAASC,EAAgBpG,GAItC,IAHA,IAAIK,EAASF,EAAUH,GACnBmG,EAAeD,EAAoBlG,GAEhCmG,GAAgBR,EAAeQ,IAA6D,WAA5C,EAAiBA,GAAclE,UACpFkE,EAAeD,EAAoBC,GAGrC,OAAIA,IAA+C,SAA9BpG,EAAYoG,IAA0D,SAA9BpG,EAAYoG,IAAwE,WAA5C,EAAiBA,GAAclE,UAC3H5B,EAGF8F,GAhDT,SAA4BnG,GAC1B,IAAIqG,EAAY,WAAWvC,KAAKd,KAGhC,GAFW,WAAWc,KAAKd,MAEfrC,EAAcX,IAII,UAFX,EAAiBA,GAEnBiC,SACb,OAAO,KAIX,IAAIqE,EAAcN,EAAchG,GAMhC,IAJIa,EAAayF,KACfA,EAAcA,EAAYb,MAGrB9E,EAAc2F,IAAgB,CAAC,OAAQ,QAAQV,QAAQ7F,EAAYuG,IAAgB,GAAG,CAC3F,IAAIC,EAAM,EAAiBD,GAI3B,GAAsB,SAAlBC,EAAIC,WAA4C,SAApBD,EAAIE,aAA0C,UAAhBF,EAAIG,UAAiF,IAA1D,CAAC,YAAa,eAAed,QAAQW,EAAII,aAAsBN,GAAgC,WAAnBE,EAAII,YAA2BN,GAAaE,EAAIK,QAAyB,SAAfL,EAAIK,OACjO,OAAON,EAEPA,EAAcA,EAAYd,UAE9B,CAEA,OAAO,IACT,CAgByBqB,CAAmB7G,IAAYK,CACxD,CCpEe,SAASyG,EAAyB3H,GAC/C,MAAO,CAAC,MAAO,UAAUyG,QAAQzG,IAAc,EAAI,IAAM,GAC3D,CCDO,SAAS4H,EAAOjE,EAAK1E,EAAOyE,GACjC,OAAO,EAAQC,EAAK,EAAQ1E,EAAOyE,GACrC,CCFe,SAASmE,EAAmBC,GACzC,OAAOxJ,OAAOkE,OAAO,CAAC,ECDf,CACLS,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GDHuC0I,EACjD,CEHe,SAASC,EAAgB9I,EAAOiD,GAC7C,OAAOA,EAAKpC,QAAO,SAAUkI,EAAS5J,GAEpC,OADA4J,EAAQ5J,GAAOa,EACR+I,CACT,GAAG,CAAC,EACN,CCuFA,SACEpG,KAAM,QACNC,SAAS,EACTC,MAAO,OACPC,GA9EF,SAAeC,GACb,IAAIiG,EAEAhG,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZmB,EAAUf,EAAKe,QACfmF,EAAejG,EAAME,SAASgB,MAC9BgF,EAAgBlG,EAAMmG,cAAcD,cACpCE,EAAgB9E,EAAiBtB,EAAMjC,WACvCsI,EAAOX,EAAyBU,GAEhCE,EADa,CAACnJ,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAClC,SAAW,QAElC,GAAKH,GAAiBC,EAAtB,CAIA,IAAIL,EAxBgB,SAAyBU,EAASvG,GAItD,OAAO4F,EAAsC,iBAH7CW,EAA6B,mBAAZA,EAAyBA,EAAQlK,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CAC/EzI,UAAWiC,EAAMjC,aACbwI,GACkDA,EAAUT,EAAgBS,EAASlJ,GAC7F,CAmBsBoJ,CAAgB3F,EAAQyF,QAASvG,GACjD0G,EAAY/C,EAAcsC,GAC1BU,EAAmB,MAATN,EAAe,EAAMlJ,EAC/ByJ,EAAmB,MAATP,EAAepJ,EAASC,EAClC2J,EAAU7G,EAAMwG,MAAM7I,UAAU2I,GAAOtG,EAAMwG,MAAM7I,UAAU0I,GAAQH,EAAcG,GAAQrG,EAAMwG,MAAM9I,OAAO4I,GAC9GQ,EAAYZ,EAAcG,GAAQrG,EAAMwG,MAAM7I,UAAU0I,GACxDU,EAAoB/B,EAAgBiB,GACpCe,EAAaD,EAA6B,MAATV,EAAeU,EAAkBE,cAAgB,EAAIF,EAAkBG,aAAe,EAAI,EAC3HC,EAAoBN,EAAU,EAAIC,EAAY,EAG9CpF,EAAMmE,EAAcc,GACpBlF,EAAMuF,EAAaN,EAAUJ,GAAOT,EAAce,GAClDQ,EAASJ,EAAa,EAAIN,EAAUJ,GAAO,EAAIa,EAC/CE,EAAS1B,EAAOjE,EAAK0F,EAAQ3F,GAE7B6F,EAAWjB,EACfrG,EAAMmG,cAAcxG,KAASqG,EAAwB,CAAC,GAAyBsB,GAAYD,EAAQrB,EAAsBuB,aAAeF,EAASD,EAAQpB,EAnBzJ,CAoBF,EA4CEtF,OA1CF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MAEdwH,EADU7G,EAAMG,QACWlC,QAC3BqH,OAAoC,IAArBuB,EAA8B,sBAAwBA,EAErD,MAAhBvB,IAKwB,iBAAjBA,IACTA,EAAejG,EAAME,SAASxC,OAAO+J,cAAcxB,MAahDpC,EAAS7D,EAAME,SAASxC,OAAQuI,KAQrCjG,EAAME,SAASgB,MAAQ+E,EACzB,EASE5E,SAAU,CAAC,iBACXqG,iBAAkB,CAAC,oBCnGN,SAASC,EAAa5J,GACnC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCOA,IAAIqG,EAAa,CACf5G,IAAK,OACL9D,MAAO,OACPD,OAAQ,OACRE,KAAM,QAeD,SAAS0K,GAAYlH,GAC1B,IAAImH,EAEApK,EAASiD,EAAMjD,OACfqK,EAAapH,EAAMoH,WACnBhK,EAAY4C,EAAM5C,UAClBiK,EAAYrH,EAAMqH,UAClBC,EAAUtH,EAAMsH,QAChBpH,EAAWF,EAAME,SACjBqH,EAAkBvH,EAAMuH,gBACxBC,EAAWxH,EAAMwH,SACjBC,EAAezH,EAAMyH,aACrBC,EAAU1H,EAAM0H,QAChBC,EAAaL,EAAQ1E,EACrBA,OAAmB,IAAf+E,EAAwB,EAAIA,EAChCC,EAAaN,EAAQxE,EACrBA,OAAmB,IAAf8E,EAAwB,EAAIA,EAEhCC,EAAgC,mBAAjBJ,EAA8BA,EAAa,CAC5D7E,EAAGA,EACHE,IACG,CACHF,EAAGA,EACHE,GAGFF,EAAIiF,EAAMjF,EACVE,EAAI+E,EAAM/E,EACV,IAAIgF,EAAOR,EAAQrL,eAAe,KAC9B8L,EAAOT,EAAQrL,eAAe,KAC9B+L,EAAQxL,EACRyL,EAAQ,EACRC,EAAM5J,OAEV,GAAIkJ,EAAU,CACZ,IAAIpD,EAAeC,EAAgBtH,GAC/BoL,EAAa,eACbC,EAAY,cAEZhE,IAAiBhG,EAAUrB,IAGmB,WAA5C,EAFJqH,EAAeN,EAAmB/G,IAECmD,UAAsC,aAAbA,IAC1DiI,EAAa,eACbC,EAAY,gBAOZhL,IAAc,IAAQA,IAAcZ,GAAQY,IAAcb,IAAU8K,IAAczK,KACpFqL,EAAQ3L,EAGRwG,IAFc4E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeD,OACzF2B,EAAa+D,IACEf,EAAW3E,OAC1BK,GAAKyE,EAAkB,GAAK,GAG1BnK,IAAcZ,IAASY,IAAc,GAAOA,IAAcd,GAAW+K,IAAczK,KACrFoL,EAAQzL,EAGRqG,IAFc8E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeH,MACzF6B,EAAagE,IACEhB,EAAW7E,MAC1BK,GAAK2E,EAAkB,GAAK,EAEhC,CAEA,IAgBMc,EAhBFC,EAAe5M,OAAOkE,OAAO,CAC/BM,SAAUA,GACTsH,GAAYP,GAEXsB,GAAyB,IAAjBd,EAlFd,SAA2BrI,EAAM8I,GAC/B,IAAItF,EAAIxD,EAAKwD,EACTE,EAAI1D,EAAK0D,EACT0F,EAAMN,EAAIO,kBAAoB,EAClC,MAAO,CACL7F,EAAG5B,EAAM4B,EAAI4F,GAAOA,GAAO,EAC3B1F,EAAG9B,EAAM8B,EAAI0F,GAAOA,GAAO,EAE/B,CA0EsCE,CAAkB,CACpD9F,EAAGA,EACHE,GACC1E,EAAUrB,IAAW,CACtB6F,EAAGA,EACHE,GAMF,OAHAF,EAAI2F,EAAM3F,EACVE,EAAIyF,EAAMzF,EAENyE,EAGK7L,OAAOkE,OAAO,CAAC,EAAG0I,IAAeD,EAAiB,CAAC,GAAkBJ,GAASF,EAAO,IAAM,GAAIM,EAAeL,GAASF,EAAO,IAAM,GAAIO,EAAe5D,WAAayD,EAAIO,kBAAoB,IAAM,EAAI,aAAe7F,EAAI,OAASE,EAAI,MAAQ,eAAiBF,EAAI,OAASE,EAAI,SAAUuF,IAG5R3M,OAAOkE,OAAO,CAAC,EAAG0I,IAAenB,EAAkB,CAAC,GAAmBc,GAASF,EAAOjF,EAAI,KAAO,GAAIqE,EAAgBa,GAASF,EAAOlF,EAAI,KAAO,GAAIuE,EAAgB1C,UAAY,GAAI0C,GAC9L,CAuDA,UACEnI,KAAM,gBACNC,SAAS,EACTC,MAAO,cACPC,GAzDF,SAAuBwJ,GACrB,IAAItJ,EAAQsJ,EAAMtJ,MACdc,EAAUwI,EAAMxI,QAChByI,EAAwBzI,EAAQoH,gBAChCA,OAA4C,IAA1BqB,GAA0CA,EAC5DC,EAAoB1I,EAAQqH,SAC5BA,OAAiC,IAAtBqB,GAAsCA,EACjDC,EAAwB3I,EAAQsH,aAChCA,OAAyC,IAA1BqB,GAA0CA,EAYzDR,EAAe,CACjBlL,UAAWuD,EAAiBtB,EAAMjC,WAClCiK,UAAWL,EAAa3H,EAAMjC,WAC9BL,OAAQsC,EAAME,SAASxC,OACvBqK,WAAY/H,EAAMwG,MAAM9I,OACxBwK,gBAAiBA,EACjBG,QAAoC,UAA3BrI,EAAMc,QAAQC,UAGgB,MAArCf,EAAMmG,cAAcD,gBACtBlG,EAAMK,OAAO3C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAO3C,OAAQmK,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACvGhB,QAASjI,EAAMmG,cAAcD,cAC7BrF,SAAUb,EAAMc,QAAQC,SACxBoH,SAAUA,EACVC,aAAcA,OAIe,MAA7BpI,EAAMmG,cAAcjF,QACtBlB,EAAMK,OAAOa,MAAQ7E,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAOa,MAAO2G,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACrGhB,QAASjI,EAAMmG,cAAcjF,MAC7BL,SAAU,WACVsH,UAAU,EACVC,aAAcA,OAIlBpI,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,wBAAyBsC,EAAMjC,WAEnC,EAQE2L,KAAM,CAAC,GChLT,IAAIC,GAAU,CACZA,SAAS,GAsCX,UACEhK,KAAM,iBACNC,SAAS,EACTC,MAAO,QACPC,GAAI,WAAe,EACnBY,OAxCF,SAAgBX,GACd,IAAIC,EAAQD,EAAKC,MACb4J,EAAW7J,EAAK6J,SAChB9I,EAAUf,EAAKe,QACf+I,EAAkB/I,EAAQgJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAkBjJ,EAAQkJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7C9K,EAASF,EAAUiB,EAAME,SAASxC,QAClCuM,EAAgB,GAAGjM,OAAOgC,EAAMiK,cAActM,UAAWqC,EAAMiK,cAAcvM,QAYjF,OAVIoM,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaC,iBAAiB,SAAUP,EAASQ,OAAQT,GAC3D,IAGEK,GACF/K,EAAOkL,iBAAiB,SAAUP,EAASQ,OAAQT,IAG9C,WACDG,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaG,oBAAoB,SAAUT,EAASQ,OAAQT,GAC9D,IAGEK,GACF/K,EAAOoL,oBAAoB,SAAUT,EAASQ,OAAQT,GAE1D,CACF,EASED,KAAM,CAAC,GC/CT,IAAIY,GAAO,CACTnN,KAAM,QACND,MAAO,OACPD,OAAQ,MACR+D,IAAK,UAEQ,SAASuJ,GAAqBxM,GAC3C,OAAOA,EAAUyM,QAAQ,0BAA0B,SAAUC,GAC3D,OAAOH,GAAKG,EACd,GACF,CCVA,IAAI,GAAO,CACTnN,MAAO,MACPC,IAAK,SAEQ,SAASmN,GAA8B3M,GACpD,OAAOA,EAAUyM,QAAQ,cAAc,SAAUC,GAC/C,OAAO,GAAKA,EACd,GACF,CCPe,SAASE,GAAgB3L,GACtC,IAAI6J,EAAM9J,EAAUC,GAGpB,MAAO,CACL4L,WAHe/B,EAAIgC,YAInBC,UAHcjC,EAAIkC,YAKtB,CCNe,SAASC,GAAoBpM,GAQ1C,OAAO+D,EAAsB8B,EAAmB7F,IAAUzB,KAAOwN,GAAgB/L,GAASgM,UAC5F,CCXe,SAASK,GAAerM,GAErC,IAAIsM,EAAoB,EAAiBtM,GACrCuM,EAAWD,EAAkBC,SAC7BC,EAAYF,EAAkBE,UAC9BC,EAAYH,EAAkBG,UAElC,MAAO,6BAA6B3I,KAAKyI,EAAWE,EAAYD,EAClE,CCLe,SAASE,GAAgBtM,GACtC,MAAI,CAAC,OAAQ,OAAQ,aAAawF,QAAQ7F,EAAYK,KAAU,EAEvDA,EAAKG,cAAcoM,KAGxBhM,EAAcP,IAASiM,GAAejM,GACjCA,EAGFsM,GAAgB1G,EAAc5F,GACvC,CCJe,SAASwM,GAAkB5M,EAAS6M,GACjD,IAAIC,OAES,IAATD,IACFA,EAAO,IAGT,IAAIvB,EAAeoB,GAAgB1M,GAC/B+M,EAASzB,KAAqE,OAAlDwB,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,MACpH1C,EAAM9J,EAAUmL,GAChB0B,EAASD,EAAS,CAAC9C,GAAK7K,OAAO6K,EAAIxF,gBAAkB,GAAI4H,GAAef,GAAgBA,EAAe,IAAMA,EAC7G2B,EAAcJ,EAAKzN,OAAO4N,GAC9B,OAAOD,EAASE,EAChBA,EAAY7N,OAAOwN,GAAkB5G,EAAcgH,IACrD,CCzBe,SAASE,GAAiBC,GACvC,OAAO1P,OAAOkE,OAAO,CAAC,EAAGwL,EAAM,CAC7B5O,KAAM4O,EAAKxI,EACXvC,IAAK+K,EAAKtI,EACVvG,MAAO6O,EAAKxI,EAAIwI,EAAK7I,MACrBjG,OAAQ8O,EAAKtI,EAAIsI,EAAK3I,QAE1B,CCqBA,SAAS4I,GAA2BpN,EAASqN,EAAgBlL,GAC3D,OAAOkL,IAAmBxO,EAAWqO,GCzBxB,SAAyBlN,EAASmC,GAC/C,IAAI8H,EAAM9J,EAAUH,GAChBsN,EAAOzH,EAAmB7F,GAC1ByE,EAAiBwF,EAAIxF,eACrBH,EAAQgJ,EAAKhF,YACb9D,EAAS8I,EAAKjF,aACd1D,EAAI,EACJE,EAAI,EAER,GAAIJ,EAAgB,CAClBH,EAAQG,EAAeH,MACvBE,EAASC,EAAeD,OACxB,IAAI+I,EAAiB1J,KAEjB0J,IAAmBA,GAA+B,UAAbpL,KACvCwC,EAAIF,EAAeG,WACnBC,EAAIJ,EAAeK,UAEvB,CAEA,MAAO,CACLR,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EAAIyH,GAAoBpM,GAC3B6E,EAAGA,EAEP,CDDwD2I,CAAgBxN,EAASmC,IAAa1B,EAAU4M,GAdxG,SAAoCrN,EAASmC,GAC3C,IAAIgL,EAAOpJ,EAAsB/D,GAAS,EAAoB,UAAbmC,GASjD,OARAgL,EAAK/K,IAAM+K,EAAK/K,IAAMpC,EAAQyN,UAC9BN,EAAK5O,KAAO4O,EAAK5O,KAAOyB,EAAQ0N,WAChCP,EAAK9O,OAAS8O,EAAK/K,IAAMpC,EAAQqI,aACjC8E,EAAK7O,MAAQ6O,EAAK5O,KAAOyB,EAAQsI,YACjC6E,EAAK7I,MAAQtE,EAAQsI,YACrB6E,EAAK3I,OAASxE,EAAQqI,aACtB8E,EAAKxI,EAAIwI,EAAK5O,KACd4O,EAAKtI,EAAIsI,EAAK/K,IACP+K,CACT,CAG0HQ,CAA2BN,EAAgBlL,GAAY+K,GEtBlK,SAAyBlN,GACtC,IAAI8M,EAEAQ,EAAOzH,EAAmB7F,GAC1B4N,EAAY7B,GAAgB/L,GAC5B2M,EAA0D,OAAlDG,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,KAChGrI,EAAQ,EAAIgJ,EAAKO,YAAaP,EAAKhF,YAAaqE,EAAOA,EAAKkB,YAAc,EAAGlB,EAAOA,EAAKrE,YAAc,GACvG9D,EAAS,EAAI8I,EAAKQ,aAAcR,EAAKjF,aAAcsE,EAAOA,EAAKmB,aAAe,EAAGnB,EAAOA,EAAKtE,aAAe,GAC5G1D,GAAKiJ,EAAU5B,WAAaI,GAAoBpM,GAChD6E,GAAK+I,EAAU1B,UAMnB,MAJiD,QAA7C,EAAiBS,GAAQW,GAAMS,YACjCpJ,GAAK,EAAI2I,EAAKhF,YAAaqE,EAAOA,EAAKrE,YAAc,GAAKhE,GAGrD,CACLA,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EACHE,EAAGA,EAEP,CFCkMmJ,CAAgBnI,EAAmB7F,IACrO,CG1Be,SAASiO,GAAe9M,GACrC,IAOIkI,EAPAtK,EAAYoC,EAAKpC,UACjBiB,EAAUmB,EAAKnB,QACfb,EAAYgC,EAAKhC,UACjBqI,EAAgBrI,EAAYuD,EAAiBvD,GAAa,KAC1DiK,EAAYjK,EAAY4J,EAAa5J,GAAa,KAClD+O,EAAUnP,EAAU4F,EAAI5F,EAAUuF,MAAQ,EAAItE,EAAQsE,MAAQ,EAC9D6J,EAAUpP,EAAU8F,EAAI9F,EAAUyF,OAAS,EAAIxE,EAAQwE,OAAS,EAGpE,OAAQgD,GACN,KAAK,EACH6B,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI7E,EAAQwE,QAE3B,MAEF,KAAKnG,EACHgL,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI9F,EAAUyF,QAE7B,MAEF,KAAKlG,EACH+K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI5F,EAAUuF,MAC3BO,EAAGsJ,GAEL,MAEF,KAAK5P,EACH8K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI3E,EAAQsE,MACzBO,EAAGsJ,GAEL,MAEF,QACE9E,EAAU,CACR1E,EAAG5F,EAAU4F,EACbE,EAAG9F,EAAU8F,GAInB,IAAIuJ,EAAW5G,EAAgBV,EAAyBU,GAAiB,KAEzE,GAAgB,MAAZ4G,EAAkB,CACpB,IAAI1G,EAAmB,MAAb0G,EAAmB,SAAW,QAExC,OAAQhF,GACN,KAAK1K,EACH2K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAC7E,MAEF,KAAK/I,EACH0K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAKnF,CAEA,OAAO2B,CACT,CC3De,SAASgF,GAAejN,EAAOc,QAC5B,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACXqM,EAAqBD,EAASnP,UAC9BA,OAAmC,IAAvBoP,EAAgCnN,EAAMjC,UAAYoP,EAC9DC,EAAoBF,EAASnM,SAC7BA,OAAiC,IAAtBqM,EAA+BpN,EAAMe,SAAWqM,EAC3DC,EAAoBH,EAASI,SAC7BA,OAAiC,IAAtBD,EAA+B7P,EAAkB6P,EAC5DE,EAAwBL,EAASM,aACjCA,OAAyC,IAA1BD,EAAmC9P,EAAW8P,EAC7DE,EAAwBP,EAASQ,eACjCA,OAA2C,IAA1BD,EAAmC/P,EAAS+P,EAC7DE,EAAuBT,EAASU,YAChCA,OAAuC,IAAzBD,GAA0CA,EACxDE,EAAmBX,EAAS3G,QAC5BA,OAA+B,IAArBsH,EAA8B,EAAIA,EAC5ChI,EAAgBD,EAAsC,iBAAZW,EAAuBA,EAAUT,EAAgBS,EAASlJ,IACpGyQ,EAAaJ,IAAmBhQ,EAASC,EAAYD,EACrDqK,EAAa/H,EAAMwG,MAAM9I,OACzBkB,EAAUoB,EAAME,SAAS0N,EAAcE,EAAaJ,GACpDK,EJkBS,SAAyBnP,EAAS0O,EAAUE,EAAczM,GACvE,IAAIiN,EAAmC,oBAAbV,EAlB5B,SAA4B1O,GAC1B,IAAIpB,EAAkBgO,GAAkB5G,EAAchG,IAElDqP,EADoB,CAAC,WAAY,SAASzJ,QAAQ,EAAiB5F,GAASiC,WAAa,GACnDtB,EAAcX,GAAWoG,EAAgBpG,GAAWA,EAE9F,OAAKS,EAAU4O,GAKRzQ,EAAgBgI,QAAO,SAAUyG,GACtC,OAAO5M,EAAU4M,IAAmBpI,EAASoI,EAAgBgC,IAAmD,SAAhCtP,EAAYsN,EAC9F,IANS,EAOX,CAK6DiC,CAAmBtP,GAAW,GAAGZ,OAAOsP,GAC/F9P,EAAkB,GAAGQ,OAAOgQ,EAAqB,CAACR,IAClDW,EAAsB3Q,EAAgB,GACtC4Q,EAAe5Q,EAAgBK,QAAO,SAAUwQ,EAASpC,GAC3D,IAAIF,EAAOC,GAA2BpN,EAASqN,EAAgBlL,GAK/D,OAJAsN,EAAQrN,IAAM,EAAI+K,EAAK/K,IAAKqN,EAAQrN,KACpCqN,EAAQnR,MAAQ,EAAI6O,EAAK7O,MAAOmR,EAAQnR,OACxCmR,EAAQpR,OAAS,EAAI8O,EAAK9O,OAAQoR,EAAQpR,QAC1CoR,EAAQlR,KAAO,EAAI4O,EAAK5O,KAAMkR,EAAQlR,MAC/BkR,CACT,GAAGrC,GAA2BpN,EAASuP,EAAqBpN,IAK5D,OAJAqN,EAAalL,MAAQkL,EAAalR,MAAQkR,EAAajR,KACvDiR,EAAahL,OAASgL,EAAanR,OAASmR,EAAapN,IACzDoN,EAAa7K,EAAI6K,EAAajR,KAC9BiR,EAAa3K,EAAI2K,EAAapN,IACvBoN,CACT,CInC2BE,CAAgBjP,EAAUT,GAAWA,EAAUA,EAAQ2P,gBAAkB9J,EAAmBzE,EAAME,SAASxC,QAAS4P,EAAUE,EAAczM,GACjKyN,EAAsB7L,EAAsB3C,EAAME,SAASvC,WAC3DuI,EAAgB2G,GAAe,CACjClP,UAAW6Q,EACX5P,QAASmJ,EACThH,SAAU,WACVhD,UAAWA,IAET0Q,EAAmB3C,GAAiBzP,OAAOkE,OAAO,CAAC,EAAGwH,EAAY7B,IAClEwI,EAAoBhB,IAAmBhQ,EAAS+Q,EAAmBD,EAGnEG,EAAkB,CACpB3N,IAAK+M,EAAmB/M,IAAM0N,EAAkB1N,IAAM6E,EAAc7E,IACpE/D,OAAQyR,EAAkBzR,OAAS8Q,EAAmB9Q,OAAS4I,EAAc5I,OAC7EE,KAAM4Q,EAAmB5Q,KAAOuR,EAAkBvR,KAAO0I,EAAc1I,KACvED,MAAOwR,EAAkBxR,MAAQ6Q,EAAmB7Q,MAAQ2I,EAAc3I,OAExE0R,EAAa5O,EAAMmG,cAAckB,OAErC,GAAIqG,IAAmBhQ,GAAUkR,EAAY,CAC3C,IAAIvH,EAASuH,EAAW7Q,GACxB1B,OAAO4D,KAAK0O,GAAiBxO,SAAQ,SAAUhE,GAC7C,IAAI0S,EAAW,CAAC3R,EAAOD,GAAQuH,QAAQrI,IAAQ,EAAI,GAAK,EACpDkK,EAAO,CAAC,EAAKpJ,GAAQuH,QAAQrI,IAAQ,EAAI,IAAM,IACnDwS,EAAgBxS,IAAQkL,EAAOhB,GAAQwI,CACzC,GACF,CAEA,OAAOF,CACT,CCyEA,UACEhP,KAAM,OACNC,SAAS,EACTC,MAAO,OACPC,GA5HF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KAEhB,IAAIK,EAAMmG,cAAcxG,GAAMmP,MAA9B,CAoCA,IAhCA,IAAIC,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAqCA,EACpDG,EAA8BtO,EAAQuO,mBACtC9I,EAAUzF,EAAQyF,QAClB+G,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtB0B,EAAwBxO,EAAQyO,eAChCA,OAA2C,IAA1BD,GAA0CA,EAC3DE,EAAwB1O,EAAQ0O,sBAChCC,EAAqBzP,EAAMc,QAAQ/C,UACnCqI,EAAgB9E,EAAiBmO,GAEjCJ,EAAqBD,IADHhJ,IAAkBqJ,GACqCF,EAjC/E,SAAuCxR,GACrC,GAAIuD,EAAiBvD,KAAeX,EAClC,MAAO,GAGT,IAAIsS,EAAoBnF,GAAqBxM,GAC7C,MAAO,CAAC2M,GAA8B3M,GAAY2R,EAAmBhF,GAA8BgF,GACrG,CA0B6IC,CAA8BF,GAA3E,CAAClF,GAAqBkF,KAChHG,EAAa,CAACH,GAAoBzR,OAAOqR,GAAoBxR,QAAO,SAAUC,EAAKC,GACrF,OAAOD,EAAIE,OAAOsD,EAAiBvD,KAAeX,ECvCvC,SAA8B4C,EAAOc,QAClC,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACX/C,EAAYmP,EAASnP,UACrBuP,EAAWJ,EAASI,SACpBE,EAAeN,EAASM,aACxBjH,EAAU2G,EAAS3G,QACnBgJ,EAAiBrC,EAASqC,eAC1BM,EAAwB3C,EAASsC,sBACjCA,OAAkD,IAA1BK,EAAmC,EAAgBA,EAC3E7H,EAAYL,EAAa5J,GACzB6R,EAAa5H,EAAYuH,EAAiB3R,EAAsBA,EAAoB4H,QAAO,SAAUzH,GACvG,OAAO4J,EAAa5J,KAAeiK,CACrC,IAAK3K,EACDyS,EAAoBF,EAAWpK,QAAO,SAAUzH,GAClD,OAAOyR,EAAsBhL,QAAQzG,IAAc,CACrD,IAEiC,IAA7B+R,EAAkBC,SACpBD,EAAoBF,GAQtB,IAAII,EAAYF,EAAkBjS,QAAO,SAAUC,EAAKC,GAOtD,OANAD,EAAIC,GAAakP,GAAejN,EAAO,CACrCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,IACRjF,EAAiBvD,IACbD,CACT,GAAG,CAAC,GACJ,OAAOzB,OAAO4D,KAAK+P,GAAWC,MAAK,SAAUC,EAAGC,GAC9C,OAAOH,EAAUE,GAAKF,EAAUG,EAClC,GACF,CDH6DC,CAAqBpQ,EAAO,CACnFjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTgJ,eAAgBA,EAChBC,sBAAuBA,IACpBzR,EACP,GAAG,IACCsS,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzB4S,EAAY,IAAIC,IAChBC,GAAqB,EACrBC,EAAwBb,EAAW,GAE9Bc,EAAI,EAAGA,EAAId,EAAWG,OAAQW,IAAK,CAC1C,IAAI3S,EAAY6R,EAAWc,GAEvBC,EAAiBrP,EAAiBvD,GAElC6S,EAAmBjJ,EAAa5J,KAAeT,EAC/CuT,EAAa,CAAC,EAAK5T,GAAQuH,QAAQmM,IAAmB,EACtDrK,EAAMuK,EAAa,QAAU,SAC7B1F,EAAW8B,GAAejN,EAAO,CACnCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdI,YAAaA,EACbrH,QAASA,IAEPuK,EAAoBD,EAAaD,EAAmB1T,EAAQC,EAAOyT,EAAmB3T,EAAS,EAE/FoT,EAAc/J,GAAOyB,EAAWzB,KAClCwK,EAAoBvG,GAAqBuG,IAG3C,IAAIC,EAAmBxG,GAAqBuG,GACxCE,EAAS,GAUb,GARIhC,GACFgC,EAAOC,KAAK9F,EAASwF,IAAmB,GAGtCxB,GACF6B,EAAOC,KAAK9F,EAAS2F,IAAsB,EAAG3F,EAAS4F,IAAqB,GAG1EC,EAAOE,OAAM,SAAUC,GACzB,OAAOA,CACT,IAAI,CACFV,EAAwB1S,EACxByS,GAAqB,EACrB,KACF,CAEAF,EAAUc,IAAIrT,EAAWiT,EAC3B,CAEA,GAAIR,EAqBF,IAnBA,IAEIa,EAAQ,SAAeC,GACzB,IAAIC,EAAmB3B,EAAW4B,MAAK,SAAUzT,GAC/C,IAAIiT,EAASV,EAAU9T,IAAIuB,GAE3B,GAAIiT,EACF,OAAOA,EAAOS,MAAM,EAAGH,GAAIJ,OAAM,SAAUC,GACzC,OAAOA,CACT,GAEJ,IAEA,GAAII,EAEF,OADAd,EAAwBc,EACjB,OAEX,EAESD,EAnBY/B,EAAiB,EAAI,EAmBZ+B,EAAK,GAGpB,UAFFD,EAAMC,GADmBA,KAOpCtR,EAAMjC,YAAc0S,IACtBzQ,EAAMmG,cAAcxG,GAAMmP,OAAQ,EAClC9O,EAAMjC,UAAY0S,EAClBzQ,EAAM0R,OAAQ,EA5GhB,CA8GF,EAQEhK,iBAAkB,CAAC,UACnBgC,KAAM,CACJoF,OAAO,IE7IX,SAAS6C,GAAexG,EAAUY,EAAM6F,GAQtC,YAPyB,IAArBA,IACFA,EAAmB,CACjBrO,EAAG,EACHE,EAAG,IAIA,CACLzC,IAAKmK,EAASnK,IAAM+K,EAAK3I,OAASwO,EAAiBnO,EACnDvG,MAAOiO,EAASjO,MAAQ6O,EAAK7I,MAAQ0O,EAAiBrO,EACtDtG,OAAQkO,EAASlO,OAAS8O,EAAK3I,OAASwO,EAAiBnO,EACzDtG,KAAMgO,EAAShO,KAAO4O,EAAK7I,MAAQ0O,EAAiBrO,EAExD,CAEA,SAASsO,GAAsB1G,GAC7B,MAAO,CAAC,EAAKjO,EAAOD,EAAQE,GAAM2U,MAAK,SAAUC,GAC/C,OAAO5G,EAAS4G,IAAS,CAC3B,GACF,CA+BA,UACEpS,KAAM,OACNC,SAAS,EACTC,MAAO,OACP6H,iBAAkB,CAAC,mBACnB5H,GAlCF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZ0Q,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBkU,EAAmB5R,EAAMmG,cAAc6L,gBACvCC,EAAoBhF,GAAejN,EAAO,CAC5C0N,eAAgB,cAEdwE,EAAoBjF,GAAejN,EAAO,CAC5C4N,aAAa,IAEXuE,EAA2BR,GAAeM,EAAmB5B,GAC7D+B,EAAsBT,GAAeO,EAAmBnK,EAAY6J,GACpES,EAAoBR,GAAsBM,GAC1CG,EAAmBT,GAAsBO,GAC7CpS,EAAMmG,cAAcxG,GAAQ,CAC1BwS,yBAA0BA,EAC1BC,oBAAqBA,EACrBC,kBAAmBA,EACnBC,iBAAkBA,GAEpBtS,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,+BAAgC2U,EAChC,sBAAuBC,GAE3B,GCJA,IACE3S,KAAM,SACNC,SAAS,EACTC,MAAO,OACPwB,SAAU,CAAC,iBACXvB,GA5BF,SAAgBa,GACd,IAAIX,EAAQW,EAAMX,MACdc,EAAUH,EAAMG,QAChBnB,EAAOgB,EAAMhB,KACb4S,EAAkBzR,EAAQuG,OAC1BA,OAA6B,IAApBkL,EAA6B,CAAC,EAAG,GAAKA,EAC/C7I,EAAO,UAAkB,SAAU5L,EAAKC,GAE1C,OADAD,EAAIC,GA5BD,SAAiCA,EAAWyI,EAAOa,GACxD,IAAIjB,EAAgB9E,EAAiBvD,GACjCyU,EAAiB,CAACrV,EAAM,GAAKqH,QAAQ4B,IAAkB,GAAK,EAAI,EAEhErG,EAAyB,mBAAXsH,EAAwBA,EAAOhL,OAAOkE,OAAO,CAAC,EAAGiG,EAAO,CACxEzI,UAAWA,KACPsJ,EACFoL,EAAW1S,EAAK,GAChB2S,EAAW3S,EAAK,GAIpB,OAFA0S,EAAWA,GAAY,EACvBC,GAAYA,GAAY,GAAKF,EACtB,CAACrV,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAAI,CACjD7C,EAAGmP,EACHjP,EAAGgP,GACD,CACFlP,EAAGkP,EACHhP,EAAGiP,EAEP,CASqBC,CAAwB5U,EAAWiC,EAAMwG,MAAOa,GAC1DvJ,CACT,GAAG,CAAC,GACA8U,EAAwBlJ,EAAK1J,EAAMjC,WACnCwF,EAAIqP,EAAsBrP,EAC1BE,EAAImP,EAAsBnP,EAEW,MAArCzD,EAAMmG,cAAcD,gBACtBlG,EAAMmG,cAAcD,cAAc3C,GAAKA,EACvCvD,EAAMmG,cAAcD,cAAczC,GAAKA,GAGzCzD,EAAMmG,cAAcxG,GAAQ+J,CAC9B,GC1BA,IACE/J,KAAM,gBACNC,SAAS,EACTC,MAAO,OACPC,GApBF,SAAuBC,GACrB,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KAKhBK,EAAMmG,cAAcxG,GAAQkN,GAAe,CACzClP,UAAWqC,EAAMwG,MAAM7I,UACvBiB,QAASoB,EAAMwG,MAAM9I,OACrBqD,SAAU,WACVhD,UAAWiC,EAAMjC,WAErB,EAQE2L,KAAM,CAAC,GCgHT,IACE/J,KAAM,kBACNC,SAAS,EACTC,MAAO,OACPC,GA/HF,SAAyBC,GACvB,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KACZoP,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAsCA,EACrD3B,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtBrH,EAAUzF,EAAQyF,QAClBsM,EAAkB/R,EAAQgS,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAwBjS,EAAQkS,aAChCA,OAAyC,IAA1BD,EAAmC,EAAIA,EACtD5H,EAAW8B,GAAejN,EAAO,CACnCsN,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTqH,YAAaA,IAEXxH,EAAgB9E,EAAiBtB,EAAMjC,WACvCiK,EAAYL,EAAa3H,EAAMjC,WAC/BkV,GAAmBjL,EACnBgF,EAAWtH,EAAyBU,GACpC8I,ECrCY,MDqCSlC,ECrCH,IAAM,IDsCxB9G,EAAgBlG,EAAMmG,cAAcD,cACpCmK,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBwV,EAA4C,mBAAjBF,EAA8BA,EAAa3W,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CACvGzI,UAAWiC,EAAMjC,aACbiV,EACFG,EAA2D,iBAAtBD,EAAiC,CACxElG,SAAUkG,EACVhE,QAASgE,GACP7W,OAAOkE,OAAO,CAChByM,SAAU,EACVkC,QAAS,GACRgE,GACCE,EAAsBpT,EAAMmG,cAAckB,OAASrH,EAAMmG,cAAckB,OAAOrH,EAAMjC,WAAa,KACjG2L,EAAO,CACTnG,EAAG,EACHE,EAAG,GAGL,GAAKyC,EAAL,CAIA,GAAI8I,EAAe,CACjB,IAAIqE,EAEAC,EAAwB,MAAbtG,EAAmB,EAAM7P,EACpCoW,EAAuB,MAAbvG,EAAmB/P,EAASC,EACtCoJ,EAAmB,MAAb0G,EAAmB,SAAW,QACpC3F,EAASnB,EAAc8G,GACvBtL,EAAM2F,EAAS8D,EAASmI,GACxB7R,EAAM4F,EAAS8D,EAASoI,GACxBC,EAAWV,GAAU/K,EAAWzB,GAAO,EAAI,EAC3CmN,EAASzL,IAAc1K,EAAQ+S,EAAc/J,GAAOyB,EAAWzB,GAC/DoN,EAAS1L,IAAc1K,GAASyK,EAAWzB,IAAQ+J,EAAc/J,GAGjEL,EAAejG,EAAME,SAASgB,MAC9BwF,EAAYoM,GAAU7M,EAAetC,EAAcsC,GAAgB,CACrE/C,MAAO,EACPE,OAAQ,GAENuQ,GAAqB3T,EAAMmG,cAAc,oBAAsBnG,EAAMmG,cAAc,oBAAoBI,QxBhFtG,CACLvF,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GwB6EFyW,GAAkBD,GAAmBL,GACrCO,GAAkBF,GAAmBJ,GAMrCO,GAAWnO,EAAO,EAAG0K,EAAc/J,GAAMI,EAAUJ,IACnDyN,GAAYd,EAAkB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWF,GAAkBT,EAA4BnG,SAAWyG,EAASK,GAAWF,GAAkBT,EAA4BnG,SACxMgH,GAAYf,GAAmB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWD,GAAkBV,EAA4BnG,SAAW0G,EAASI,GAAWD,GAAkBV,EAA4BnG,SACzMjG,GAAoB/G,EAAME,SAASgB,OAAS8D,EAAgBhF,EAAME,SAASgB,OAC3E+S,GAAelN,GAAiC,MAAbiG,EAAmBjG,GAAkBsF,WAAa,EAAItF,GAAkBuF,YAAc,EAAI,EAC7H4H,GAAwH,OAAjGb,EAA+C,MAAvBD,OAA8B,EAASA,EAAoBpG,IAAqBqG,EAAwB,EAEvJc,GAAY9M,EAAS2M,GAAYE,GACjCE,GAAkBzO,EAAOmN,EAAS,EAAQpR,EAF9B2F,EAAS0M,GAAYG,GAAsBD,IAEKvS,EAAK2F,EAAQyL,EAAS,EAAQrR,EAAK0S,IAAa1S,GAChHyE,EAAc8G,GAAYoH,GAC1B1K,EAAKsD,GAAYoH,GAAkB/M,CACrC,CAEA,GAAI8H,EAAc,CAChB,IAAIkF,GAEAC,GAAyB,MAAbtH,EAAmB,EAAM7P,EAErCoX,GAAwB,MAAbvH,EAAmB/P,EAASC,EAEvCsX,GAAUtO,EAAcgJ,GAExBuF,GAAmB,MAAZvF,EAAkB,SAAW,QAEpCwF,GAAOF,GAAUrJ,EAASmJ,IAE1BK,GAAOH,GAAUrJ,EAASoJ,IAE1BK,IAAuD,IAAxC,CAAC,EAAKzX,GAAMqH,QAAQ4B,GAEnCyO,GAAyH,OAAjGR,GAAgD,MAAvBjB,OAA8B,EAASA,EAAoBlE,IAAoBmF,GAAyB,EAEzJS,GAAaF,GAAeF,GAAOF,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAEzI6F,GAAaH,GAAeJ,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAAUyF,GAE5IK,GAAmBlC,GAAU8B,G1BzH9B,SAAwBlT,EAAK1E,EAAOyE,GACzC,IAAIwT,EAAItP,EAAOjE,EAAK1E,EAAOyE,GAC3B,OAAOwT,EAAIxT,EAAMA,EAAMwT,CACzB,C0BsHoDC,CAAeJ,GAAYN,GAASO,IAAcpP,EAAOmN,EAASgC,GAAaJ,GAAMF,GAAS1B,EAASiC,GAAaJ,IAEpKzO,EAAcgJ,GAAW8F,GACzBtL,EAAKwF,GAAW8F,GAAmBR,EACrC,CAEAxU,EAAMmG,cAAcxG,GAAQ+J,CAvE5B,CAwEF,EAQEhC,iBAAkB,CAAC,WE1HN,SAASyN,GAAiBC,EAAyBrQ,EAAcsD,QAC9D,IAAZA,IACFA,GAAU,GAGZ,ICnBoCrJ,ECJOJ,EFuBvCyW,EAA0B9V,EAAcwF,GACxCuQ,EAAuB/V,EAAcwF,IAf3C,SAAyBnG,GACvB,IAAImN,EAAOnN,EAAQ+D,wBACfI,EAASpB,EAAMoK,EAAK7I,OAAStE,EAAQqE,aAAe,EACpDD,EAASrB,EAAMoK,EAAK3I,QAAUxE,EAAQuE,cAAgB,EAC1D,OAAkB,IAAXJ,GAA2B,IAAXC,CACzB,CAU4DuS,CAAgBxQ,GACtEJ,EAAkBF,EAAmBM,GACrCgH,EAAOpJ,EAAsByS,EAAyBE,EAAsBjN,GAC5EyB,EAAS,CACXc,WAAY,EACZE,UAAW,GAET7C,EAAU,CACZ1E,EAAG,EACHE,EAAG,GAkBL,OAfI4R,IAA4BA,IAA4BhN,MACxB,SAA9B1J,EAAYoG,IAChBkG,GAAetG,MACbmF,GCnCgC9K,EDmCT+F,KClCdhG,EAAUC,IAAUO,EAAcP,GCJxC,CACL4L,YAFyChM,EDQbI,GCNR4L,WACpBE,UAAWlM,EAAQkM,WDGZH,GAAgB3L,IDoCnBO,EAAcwF,KAChBkD,EAAUtF,EAAsBoC,GAAc,IACtCxB,GAAKwB,EAAauH,WAC1BrE,EAAQxE,GAAKsB,EAAasH,WACjB1H,IACTsD,EAAQ1E,EAAIyH,GAAoBrG,KAI7B,CACLpB,EAAGwI,EAAK5O,KAAO2M,EAAOc,WAAa3C,EAAQ1E,EAC3CE,EAAGsI,EAAK/K,IAAM8I,EAAOgB,UAAY7C,EAAQxE,EACzCP,MAAO6I,EAAK7I,MACZE,OAAQ2I,EAAK3I,OAEjB,CGvDA,SAASoS,GAAMC,GACb,IAAItT,EAAM,IAAIoO,IACVmF,EAAU,IAAIC,IACdC,EAAS,GAKb,SAAS3F,EAAK4F,GACZH,EAAQI,IAAID,EAASlW,MACN,GAAG3B,OAAO6X,EAASxU,UAAY,GAAIwU,EAASnO,kBAAoB,IACtEvH,SAAQ,SAAU4V,GACzB,IAAKL,EAAQM,IAAID,GAAM,CACrB,IAAIE,EAAc9T,EAAI3F,IAAIuZ,GAEtBE,GACFhG,EAAKgG,EAET,CACF,IACAL,EAAO3E,KAAK4E,EACd,CAQA,OAzBAJ,EAAUtV,SAAQ,SAAU0V,GAC1B1T,EAAIiP,IAAIyE,EAASlW,KAAMkW,EACzB,IAiBAJ,EAAUtV,SAAQ,SAAU0V,GACrBH,EAAQM,IAAIH,EAASlW,OAExBsQ,EAAK4F,EAET,IACOD,CACT,CClBA,IAEIM,GAAkB,CACpBnY,UAAW,SACX0X,UAAW,GACX1U,SAAU,YAGZ,SAASoV,KACP,IAAK,IAAI1B,EAAO2B,UAAUrG,OAAQsG,EAAO,IAAIpU,MAAMwS,GAAO6B,EAAO,EAAGA,EAAO7B,EAAM6B,IAC/ED,EAAKC,GAAQF,UAAUE,GAGzB,OAAQD,EAAKvE,MAAK,SAAUlT,GAC1B,QAASA,GAAoD,mBAAlCA,EAAQ+D,sBACrC,GACF,CAEO,SAAS4T,GAAgBC,QACL,IAArBA,IACFA,EAAmB,CAAC,GAGtB,IAAIC,EAAoBD,EACpBE,EAAwBD,EAAkBE,iBAC1CA,OAA6C,IAA1BD,EAAmC,GAAKA,EAC3DE,EAAyBH,EAAkBI,eAC3CA,OAA4C,IAA3BD,EAAoCV,GAAkBU,EAC3E,OAAO,SAAsBjZ,EAAWD,EAAQoD,QAC9B,IAAZA,IACFA,EAAU+V,GAGZ,IC/C6B/W,EAC3BgX,ED8CE9W,EAAQ,CACVjC,UAAW,SACXgZ,iBAAkB,GAClBjW,QAASzE,OAAOkE,OAAO,CAAC,EAAG2V,GAAiBW,GAC5C1Q,cAAe,CAAC,EAChBjG,SAAU,CACRvC,UAAWA,EACXD,OAAQA,GAEV4C,WAAY,CAAC,EACbD,OAAQ,CAAC,GAEP2W,EAAmB,GACnBC,GAAc,EACdrN,EAAW,CACb5J,MAAOA,EACPkX,WAAY,SAAoBC,GAC9B,IAAIrW,EAAsC,mBAArBqW,EAAkCA,EAAiBnX,EAAMc,SAAWqW,EACzFC,IACApX,EAAMc,QAAUzE,OAAOkE,OAAO,CAAC,EAAGsW,EAAgB7W,EAAMc,QAASA,GACjEd,EAAMiK,cAAgB,CACpBtM,UAAW0B,EAAU1B,GAAa6N,GAAkB7N,GAAaA,EAAU4Q,eAAiB/C,GAAkB7N,EAAU4Q,gBAAkB,GAC1I7Q,OAAQ8N,GAAkB9N,IAI5B,IEzE4B+X,EAC9B4B,EFwEMN,EDvCG,SAAwBtB,GAErC,IAAIsB,EAAmBvB,GAAMC,GAE7B,OAAO/W,EAAeb,QAAO,SAAUC,EAAK+B,GAC1C,OAAO/B,EAAIE,OAAO+Y,EAAiBvR,QAAO,SAAUqQ,GAClD,OAAOA,EAAShW,QAAUA,CAC5B,IACF,GAAG,GACL,CC8B+ByX,EEzEK7B,EFyEsB,GAAGzX,OAAO2Y,EAAkB3W,EAAMc,QAAQ2U,WExE9F4B,EAAS5B,EAAU5X,QAAO,SAAUwZ,EAAQE,GAC9C,IAAIC,EAAWH,EAAOE,EAAQ5X,MAK9B,OAJA0X,EAAOE,EAAQ5X,MAAQ6X,EAAWnb,OAAOkE,OAAO,CAAC,EAAGiX,EAAUD,EAAS,CACrEzW,QAASzE,OAAOkE,OAAO,CAAC,EAAGiX,EAAS1W,QAASyW,EAAQzW,SACrD4I,KAAMrN,OAAOkE,OAAO,CAAC,EAAGiX,EAAS9N,KAAM6N,EAAQ7N,QAC5C6N,EACEF,CACT,GAAG,CAAC,GAEGhb,OAAO4D,KAAKoX,GAAQlV,KAAI,SAAUhG,GACvC,OAAOkb,EAAOlb,EAChB,MFsGM,OAvCA6D,EAAM+W,iBAAmBA,EAAiBvR,QAAO,SAAUiS,GACzD,OAAOA,EAAE7X,OACX,IAoJFI,EAAM+W,iBAAiB5W,SAAQ,SAAUqI,GACvC,IAAI7I,EAAO6I,EAAM7I,KACb+X,EAAgBlP,EAAM1H,QACtBA,OAA4B,IAAlB4W,EAA2B,CAAC,EAAIA,EAC1ChX,EAAS8H,EAAM9H,OAEnB,GAAsB,mBAAXA,EAAuB,CAChC,IAAIiX,EAAYjX,EAAO,CACrBV,MAAOA,EACPL,KAAMA,EACNiK,SAAUA,EACV9I,QAASA,IAKXkW,EAAiB/F,KAAK0G,GAFT,WAAmB,EAGlC,CACF,IAjIS/N,EAASQ,QAClB,EAMAwN,YAAa,WACX,IAAIX,EAAJ,CAIA,IAAIY,EAAkB7X,EAAME,SACxBvC,EAAYka,EAAgBla,UAC5BD,EAASma,EAAgBna,OAG7B,GAAKyY,GAAiBxY,EAAWD,GAAjC,CASAsC,EAAMwG,MAAQ,CACZ7I,UAAWwX,GAAiBxX,EAAWqH,EAAgBtH,GAAoC,UAA3BsC,EAAMc,QAAQC,UAC9ErD,OAAQiG,EAAcjG,IAOxBsC,EAAM0R,OAAQ,EACd1R,EAAMjC,UAAYiC,EAAMc,QAAQ/C,UAKhCiC,EAAM+W,iBAAiB5W,SAAQ,SAAU0V,GACvC,OAAO7V,EAAMmG,cAAc0P,EAASlW,MAAQtD,OAAOkE,OAAO,CAAC,EAAGsV,EAASnM,KACzE,IAGA,IAFA,IAESoO,EAAQ,EAAGA,EAAQ9X,EAAM+W,iBAAiBhH,OAAQ+H,IAUzD,IAAoB,IAAhB9X,EAAM0R,MAAV,CAMA,IAAIqG,EAAwB/X,EAAM+W,iBAAiBe,GAC/ChY,EAAKiY,EAAsBjY,GAC3BkY,EAAyBD,EAAsBjX,QAC/CoM,OAAsC,IAA3B8K,EAAoC,CAAC,EAAIA,EACpDrY,EAAOoY,EAAsBpY,KAEf,mBAAPG,IACTE,EAAQF,EAAG,CACTE,MAAOA,EACPc,QAASoM,EACTvN,KAAMA,EACNiK,SAAUA,KACN5J,EAdR,MAHEA,EAAM0R,OAAQ,EACdoG,GAAS,CAnCb,CAbA,CAmEF,EAGA1N,QClM2BtK,EDkMV,WACf,OAAO,IAAImY,SAAQ,SAAUC,GAC3BtO,EAASgO,cACTM,EAAQlY,EACV,GACF,ECrMG,WAUL,OATK8W,IACHA,EAAU,IAAImB,SAAQ,SAAUC,GAC9BD,QAAQC,UAAUC,MAAK,WACrBrB,OAAUsB,EACVF,EAAQpY,IACV,GACF,KAGKgX,CACT,GD2LIuB,QAAS,WACPjB,IACAH,GAAc,CAChB,GAGF,IAAKd,GAAiBxY,EAAWD,GAK/B,OAAOkM,EAmCT,SAASwN,IACPJ,EAAiB7W,SAAQ,SAAUL,GACjC,OAAOA,GACT,IACAkX,EAAmB,EACrB,CAEA,OAvCApN,EAASsN,WAAWpW,GAASqX,MAAK,SAAUnY,IACrCiX,GAAenW,EAAQwX,eAC1BxX,EAAQwX,cAActY,EAE1B,IAmCO4J,CACT,CACF,CACO,IAAI2O,GAA4BhC,KGrPnC,GAA4BA,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,EAAa,GAAQ,GAAM,GAAiB,EAAO,MCJrH,GAA4BjC,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,KCQtE,MAEMC,GAAiB,gBAsBjBC,GAAc9Z,IAClB,IAAI+Z,EAAW/Z,EAAQga,aAAa,kBAEpC,IAAKD,GAAyB,MAAbA,EAAkB,CACjC,IAAIE,EAAgBja,EAAQga,aAAa,QAKzC,IAAKC,IAAkBA,EAAcC,SAAS,OAASD,EAAcE,WAAW,KAC9E,OAAO,KAILF,EAAcC,SAAS,OAASD,EAAcE,WAAW,OAC3DF,EAAgB,IAAIA,EAActX,MAAM,KAAK,MAG/CoX,EAAWE,GAAmC,MAAlBA,EAAwBA,EAAcG,OAAS,IAC7E,CAEA,OAAOL,CAAQ,EAGXM,GAAyBra,IAC7B,MAAM+Z,EAAWD,GAAY9Z,GAE7B,OAAI+Z,GACKjU,SAAS+C,cAAckR,GAAYA,EAGrC,IAAI,EAGPO,GAAyBta,IAC7B,MAAM+Z,EAAWD,GAAY9Z,GAC7B,OAAO+Z,EAAWjU,SAAS+C,cAAckR,GAAY,IAAI,EA0BrDQ,GAAuBva,IAC3BA,EAAQwa,cAAc,IAAIC,MAAMZ,IAAgB,EAG5C,GAAYa,MACXA,GAA4B,iBAAXA,UAIO,IAAlBA,EAAOC,SAChBD,EAASA,EAAO,SAGgB,IAApBA,EAAOE,UAGjBC,GAAaH,GAEb,GAAUA,GACLA,EAAOC,OAASD,EAAO,GAAKA,EAGf,iBAAXA,GAAuBA,EAAOvJ,OAAS,EACzCrL,SAAS+C,cAAc6R,GAGzB,KAGHI,GAAY9a,IAChB,IAAK,GAAUA,IAAgD,IAApCA,EAAQ+a,iBAAiB5J,OAClD,OAAO,EAGT,MAAM6J,EAAgF,YAA7DtV,iBAAiB1F,GAASib,iBAAiB,cAE9DC,EAAgBlb,EAAQmb,QAAQ,uBAEtC,IAAKD,EACH,OAAOF,EAGT,GAAIE,IAAkBlb,EAAS,CAC7B,MAAMob,EAAUpb,EAAQmb,QAAQ,WAEhC,GAAIC,GAAWA,EAAQ5V,aAAe0V,EACpC,OAAO,EAGT,GAAgB,OAAZE,EACF,OAAO,CAEX,CAEA,OAAOJ,CAAgB,EAGnBK,GAAarb,IACZA,GAAWA,EAAQ4a,WAAaU,KAAKC,gBAItCvb,EAAQwb,UAAUvW,SAAS,mBAIC,IAArBjF,EAAQyb,SACVzb,EAAQyb,SAGVzb,EAAQ0b,aAAa,aAAoD,UAArC1b,EAAQga,aAAa,aAG5D2B,GAAiB3b,IACrB,IAAK8F,SAASC,gBAAgB6V,aAC5B,OAAO,KAIT,GAAmC,mBAAxB5b,EAAQqF,YAA4B,CAC7C,MAAMwW,EAAO7b,EAAQqF,cACrB,OAAOwW,aAAgB/a,WAAa+a,EAAO,IAC7C,CAEA,OAAI7b,aAAmBc,WACdd,EAIJA,EAAQwF,WAINmW,GAAe3b,EAAQwF,YAHrB,IAGgC,EAGrCsW,GAAO,OAWPC,GAAS/b,IACbA,EAAQuE,YAAY,EAGhByX,GAAY,IACZ3b,OAAO4b,SAAWnW,SAAS6G,KAAK+O,aAAa,qBACxCrb,OAAO4b,OAGT,KAGHC,GAA4B,GAmB5BC,GAAQ,IAAuC,QAAjCrW,SAASC,gBAAgBqW,IAEvCC,GAAqBC,IAnBAC,QAoBN,KACjB,MAAMC,EAAIR,KAGV,GAAIQ,EAAG,CACL,MAAMzb,EAAOub,EAAOG,KACdC,EAAqBF,EAAEtb,GAAGH,GAChCyb,EAAEtb,GAAGH,GAAQub,EAAOK,gBACpBH,EAAEtb,GAAGH,GAAM6b,YAAcN,EAEzBE,EAAEtb,GAAGH,GAAM8b,WAAa,KACtBL,EAAEtb,GAAGH,GAAQ2b,EACNJ,EAAOK,gBAElB,GAjC0B,YAAxB7W,SAASgX,YAENZ,GAA0B/K,QAC7BrL,SAASyF,iBAAiB,oBAAoB,KAC5C,IAAK,MAAMgR,KAAYL,GACrBK,GACF,IAIJL,GAA0B7J,KAAKkK,IAE/BA,GAsBA,EAGEQ,GAAUR,IACU,mBAAbA,GACTA,GACF,EAGIS,GAAyB,CAACT,EAAUU,EAAmBC,GAAoB,KAC/E,IAAKA,EAEH,YADAH,GAAQR,GAIV,MACMY,EAnMiCnd,KACvC,IAAKA,EACH,OAAO,EAIT,IAAI,mBACFod,EAAkB,gBAClBC,GACEhd,OAAOqF,iBAAiB1F,GAC5B,MAAMsd,EAA0BC,OAAOC,WAAWJ,GAC5CK,EAAuBF,OAAOC,WAAWH,GAE/C,OAAKC,GAA4BG,GAKjCL,EAAqBA,EAAmBza,MAAM,KAAK,GACnD0a,EAAkBA,EAAgB1a,MAAM,KAAK,GAjFf,KAkFtB4a,OAAOC,WAAWJ,GAAsBG,OAAOC,WAAWH,KANzD,CAMoG,EA+KpFK,CAAiCT,GADlC,EAExB,IAAIU,GAAS,EAEb,MAAMC,EAAU,EACd5Q,aAEIA,IAAWiQ,IAIfU,GAAS,EACTV,EAAkBxR,oBAAoBoO,GAAgB+D,GACtDb,GAAQR,GAAS,EAGnBU,EAAkB1R,iBAAiBsO,GAAgB+D,GACnDC,YAAW,KACJF,GACHpD,GAAqB0C,EACvB,GACCE,EAAiB,EAahBW,GAAuB,CAACjR,EAAMkR,EAAeC,EAAeC,KAChE,MAAMC,EAAarR,EAAKsE,OACxB,IAAI+H,EAAQrM,EAAKjH,QAAQmY,GAGzB,OAAe,IAAX7E,GACM8E,GAAiBC,EAAiBpR,EAAKqR,EAAa,GAAKrR,EAAK,IAGxEqM,GAAS8E,EAAgB,GAAK,EAE1BC,IACF/E,GAASA,EAAQgF,GAAcA,GAG1BrR,EAAKjK,KAAKC,IAAI,EAAGD,KAAKE,IAAIoW,EAAOgF,EAAa,KAAI,EAarDC,GAAiB,qBACjBC,GAAiB,OACjBC,GAAgB,SAChBC,GAAgB,CAAC,EAEvB,IAAIC,GAAW,EACf,MAAMC,GAAe,CACnBC,WAAY,YACZC,WAAY,YAERC,GAAe,IAAI5H,IAAI,CAAC,QAAS,WAAY,UAAW,YAAa,cAAe,aAAc,iBAAkB,YAAa,WAAY,YAAa,cAAe,YAAa,UAAW,WAAY,QAAS,oBAAqB,aAAc,YAAa,WAAY,cAAe,cAAe,cAAe,YAAa,eAAgB,gBAAiB,eAAgB,gBAAiB,aAAc,QAAS,OAAQ,SAAU,QAAS,SAAU,SAAU,UAAW,WAAY,OAAQ,SAAU,eAAgB,SAAU,OAAQ,mBAAoB,mBAAoB,QAAS,QAAS,WAK/lB,SAAS6H,GAAa5e,EAAS6e,GAC7B,OAAOA,GAAO,GAAGA,MAAQN,QAAgBve,EAAQue,UAAYA,IAC/D,CAEA,SAASO,GAAiB9e,GACxB,MAAM6e,EAAMD,GAAa5e,GAGzB,OAFAA,EAAQue,SAAWM,EACnBP,GAAcO,GAAOP,GAAcO,IAAQ,CAAC,EACrCP,GAAcO,EACvB,CA0CA,SAASE,GAAYC,EAAQC,EAAUC,EAAqB,MAC1D,OAAOzhB,OAAO0hB,OAAOH,GAAQpM,MAAKwM,GAASA,EAAMH,WAAaA,GAAYG,EAAMF,qBAAuBA,GACzG,CAEA,SAASG,GAAoBC,EAAmB1B,EAAS2B,GACvD,MAAMC,EAAiC,iBAAZ5B,EAErBqB,EAAWO,EAAcD,EAAqB3B,GAAW2B,EAC/D,IAAIE,EAAYC,GAAaJ,GAM7B,OAJKX,GAAavH,IAAIqI,KACpBA,EAAYH,GAGP,CAACE,EAAaP,EAAUQ,EACjC,CAEA,SAASE,GAAW3f,EAASsf,EAAmB1B,EAAS2B,EAAoBK,GAC3E,GAAiC,iBAAtBN,IAAmCtf,EAC5C,OAGF,IAAKwf,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GAGzF,GAAID,KAAqBd,GAAc,CACrC,MAAMqB,EAAe3e,GACZ,SAAUke,GACf,IAAKA,EAAMU,eAAiBV,EAAMU,gBAAkBV,EAAMW,iBAAmBX,EAAMW,eAAe9a,SAASma,EAAMU,eAC/G,OAAO5e,EAAGjD,KAAK+hB,KAAMZ,EAEzB,EAGFH,EAAWY,EAAaZ,EAC1B,CAEA,MAAMD,EAASF,GAAiB9e,GAC1BigB,EAAWjB,EAAOS,KAAeT,EAAOS,GAAa,CAAC,GACtDS,EAAmBnB,GAAYkB,EAAUhB,EAAUO,EAAc5B,EAAU,MAEjF,GAAIsC,EAEF,YADAA,EAAiBN,OAASM,EAAiBN,QAAUA,GAIvD,MAAMf,EAAMD,GAAaK,EAAUK,EAAkB1T,QAAQuS,GAAgB,KACvEjd,EAAKse,EAzEb,SAAoCxf,EAAS+Z,EAAU7Y,GACrD,OAAO,SAAS0c,EAAQwB,GACtB,MAAMe,EAAcngB,EAAQogB,iBAAiBrG,GAE7C,IAAK,IAAI,OACP/M,GACEoS,EAAOpS,GAAUA,IAAWgT,KAAMhT,EAASA,EAAOxH,WACpD,IAAK,MAAM6a,KAAcF,EACvB,GAAIE,IAAerT,EAYnB,OARAsT,GAAWlB,EAAO,CAChBW,eAAgB/S,IAGd4Q,EAAQgC,QACVW,GAAaC,IAAIxgB,EAASof,EAAMqB,KAAM1G,EAAU7Y,GAG3CA,EAAGwf,MAAM1T,EAAQ,CAACoS,GAG/B,CACF,CAiD2BuB,CAA2B3gB,EAAS4d,EAASqB,GAvFxE,SAA0Bjf,EAASkB,GACjC,OAAO,SAAS0c,EAAQwB,GAStB,OARAkB,GAAWlB,EAAO,CAChBW,eAAgB/f,IAGd4d,EAAQgC,QACVW,GAAaC,IAAIxgB,EAASof,EAAMqB,KAAMvf,GAGjCA,EAAGwf,MAAM1gB,EAAS,CAACof,GAC5B,CACF,CA2EoFwB,CAAiB5gB,EAASif,GAC5G/d,EAAGge,mBAAqBM,EAAc5B,EAAU,KAChD1c,EAAG+d,SAAWA,EACd/d,EAAG0e,OAASA,EACZ1e,EAAGqd,SAAWM,EACdoB,EAASpB,GAAO3d,EAChBlB,EAAQuL,iBAAiBkU,EAAWve,EAAIse,EAC1C,CAEA,SAASqB,GAAc7gB,EAASgf,EAAQS,EAAW7B,EAASsB,GAC1D,MAAMhe,EAAK6d,GAAYC,EAAOS,GAAY7B,EAASsB,GAE9Che,IAILlB,EAAQyL,oBAAoBgU,EAAWve,EAAI4f,QAAQ5B,WAC5CF,EAAOS,GAAWve,EAAGqd,UAC9B,CAEA,SAASwC,GAAyB/gB,EAASgf,EAAQS,EAAWuB,GAC5D,MAAMC,EAAoBjC,EAAOS,IAAc,CAAC,EAEhD,IAAK,MAAMyB,KAAczjB,OAAO4D,KAAK4f,GACnC,GAAIC,EAAWhH,SAAS8G,GAAY,CAClC,MAAM5B,EAAQ6B,EAAkBC,GAChCL,GAAc7gB,EAASgf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAClE,CAEJ,CAEA,SAASQ,GAAaN,GAGpB,OADAA,EAAQA,EAAMxT,QAAQwS,GAAgB,IAC/BI,GAAaY,IAAUA,CAChC,CAEA,MAAMmB,GAAe,CACnBY,GAAGnhB,EAASof,EAAOxB,EAAS2B,GAC1BI,GAAW3f,EAASof,EAAOxB,EAAS2B,GAAoB,EAC1D,EAEA6B,IAAIphB,EAASof,EAAOxB,EAAS2B,GAC3BI,GAAW3f,EAASof,EAAOxB,EAAS2B,GAAoB,EAC1D,EAEAiB,IAAIxgB,EAASsf,EAAmB1B,EAAS2B,GACvC,GAAiC,iBAAtBD,IAAmCtf,EAC5C,OAGF,MAAOwf,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GACrF8B,EAAc5B,IAAcH,EAC5BN,EAASF,GAAiB9e,GAC1BihB,EAAoBjC,EAAOS,IAAc,CAAC,EAC1C6B,EAAchC,EAAkBnF,WAAW,KAEjD,QAAwB,IAAb8E,EAAX,CAUA,GAAIqC,EACF,IAAK,MAAMC,KAAgB9jB,OAAO4D,KAAK2d,GACrC+B,GAAyB/gB,EAASgf,EAAQuC,EAAcjC,EAAkBzM,MAAM,IAIpF,IAAK,MAAM2O,KAAe/jB,OAAO4D,KAAK4f,GAAoB,CACxD,MAAMC,EAAaM,EAAY5V,QAAQyS,GAAe,IAEtD,IAAKgD,GAAe/B,EAAkBpF,SAASgH,GAAa,CAC1D,MAAM9B,EAAQ6B,EAAkBO,GAChCX,GAAc7gB,EAASgf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAClE,CACF,CAfA,KARA,CAEE,IAAKzhB,OAAO4D,KAAK4f,GAAmB9P,OAClC,OAGF0P,GAAc7gB,EAASgf,EAAQS,EAAWR,EAAUO,EAAc5B,EAAU,KAE9E,CAgBF,EAEA6D,QAAQzhB,EAASof,EAAO3H,GACtB,GAAqB,iBAAV2H,IAAuBpf,EAChC,OAAO,KAGT,MAAMwc,EAAIR,KAGV,IAAI0F,EAAc,KACdC,GAAU,EACVC,GAAiB,EACjBC,GAAmB,EAJHzC,IADFM,GAAaN,IAOZ5C,IACjBkF,EAAclF,EAAE/B,MAAM2E,EAAO3H,GAC7B+E,EAAExc,GAASyhB,QAAQC,GACnBC,GAAWD,EAAYI,uBACvBF,GAAkBF,EAAYK,gCAC9BF,EAAmBH,EAAYM,sBAGjC,IAAIC,EAAM,IAAIxH,MAAM2E,EAAO,CACzBuC,UACAO,YAAY,IAgBd,OAdAD,EAAM3B,GAAW2B,EAAKxK,GAElBoK,GACFI,EAAIE,iBAGFP,GACF5hB,EAAQwa,cAAcyH,GAGpBA,EAAIJ,kBAAoBH,GAC1BA,EAAYS,iBAGPF,CACT,GAIF,SAAS3B,GAAWziB,EAAKukB,GACvB,IAAK,MAAO7kB,EAAKa,KAAUX,OAAO4kB,QAAQD,GAAQ,CAAC,GACjD,IACEvkB,EAAIN,GAAOa,CACb,CAAE,MAAOkkB,GACP7kB,OAAOC,eAAeG,EAAKN,EAAK,CAC9BglB,cAAc,EAEd3kB,IAAG,IACMQ,GAIb,CAGF,OAAOP,CACT,CAYA,MAAM2kB,GAAa,IAAI7Q,IACjB8Q,GAAO,CACXjQ,IAAIxS,EAASzC,EAAKyN,GACXwX,GAAWpL,IAAIpX,IAClBwiB,GAAWhQ,IAAIxS,EAAS,IAAI2R,KAG9B,MAAM+Q,EAAcF,GAAW5kB,IAAIoC,GAG9B0iB,EAAYtL,IAAI7Z,IAA6B,IAArBmlB,EAAYC,KAMzCD,EAAYlQ,IAAIjV,EAAKyN,GAJnB4X,QAAQC,MAAM,+EAA+Exf,MAAMyf,KAAKJ,EAAYrhB,QAAQ,MAKhI,EAEAzD,IAAG,CAACoC,EAASzC,IACPilB,GAAWpL,IAAIpX,IACVwiB,GAAW5kB,IAAIoC,GAASpC,IAAIL,IAG9B,KAGTwlB,OAAO/iB,EAASzC,GACd,IAAKilB,GAAWpL,IAAIpX,GAClB,OAGF,MAAM0iB,EAAcF,GAAW5kB,IAAIoC,GACnC0iB,EAAYM,OAAOzlB,GAEM,IAArBmlB,EAAYC,MACdH,GAAWQ,OAAOhjB,EAEtB,GAUF,SAASijB,GAAc7kB,GACrB,GAAc,SAAVA,EACF,OAAO,EAGT,GAAc,UAAVA,EACF,OAAO,EAGT,GAAIA,IAAUmf,OAAOnf,GAAOkC,WAC1B,OAAOid,OAAOnf,GAGhB,GAAc,KAAVA,GAA0B,SAAVA,EAClB,OAAO,KAGT,GAAqB,iBAAVA,EACT,OAAOA,EAGT,IACE,OAAO8kB,KAAKC,MAAMC,mBAAmBhlB,GACvC,CAAE,MAAOkkB,GACP,OAAOlkB,CACT,CACF,CAEA,SAASilB,GAAiB9lB,GACxB,OAAOA,EAAIqO,QAAQ,UAAU0X,GAAO,IAAIA,EAAIpjB,iBAC9C,CAEA,MAAMqjB,GAAc,CAClBC,iBAAiBxjB,EAASzC,EAAKa,GAC7B4B,EAAQ6B,aAAa,WAAWwhB,GAAiB9lB,KAAQa,EAC3D,EAEAqlB,oBAAoBzjB,EAASzC,GAC3ByC,EAAQ4B,gBAAgB,WAAWyhB,GAAiB9lB,KACtD,EAEAmmB,kBAAkB1jB,GAChB,IAAKA,EACH,MAAO,CAAC,EAGV,MAAM0B,EAAa,CAAC,EACdiiB,EAASlmB,OAAO4D,KAAKrB,EAAQ4jB,SAAShd,QAAOrJ,GAAOA,EAAI4c,WAAW,QAAU5c,EAAI4c,WAAW,cAElG,IAAK,MAAM5c,KAAOomB,EAAQ,CACxB,IAAIE,EAAUtmB,EAAIqO,QAAQ,MAAO,IACjCiY,EAAUA,EAAQC,OAAO,GAAG5jB,cAAgB2jB,EAAQhR,MAAM,EAAGgR,EAAQ1S,QACrEzP,EAAWmiB,GAAWZ,GAAcjjB,EAAQ4jB,QAAQrmB,GACtD,CAEA,OAAOmE,CACT,EAEAqiB,iBAAgB,CAAC/jB,EAASzC,IACjB0lB,GAAcjjB,EAAQga,aAAa,WAAWqJ,GAAiB9lB,QAe1E,MAAMymB,GAEOC,qBACT,MAAO,CAAC,CACV,CAEWC,yBACT,MAAO,CAAC,CACV,CAEWzH,kBACT,MAAM,IAAI0H,MAAM,sEAClB,CAEAC,WAAWC,GAMT,OALAA,EAASrE,KAAKsE,gBAAgBD,GAC9BA,EAASrE,KAAKuE,kBAAkBF,GAEhCrE,KAAKwE,iBAAiBH,GAEfA,CACT,CAEAE,kBAAkBF,GAChB,OAAOA,CACT,CAEAC,gBAAgBD,EAAQrkB,GACtB,MAAMykB,EAAa,GAAUzkB,GAAWujB,GAAYQ,iBAAiB/jB,EAAS,UAAY,CAAC,EAE3F,MAAO,IAAKggB,KAAK0E,YAAYT,WACD,iBAAfQ,EAA0BA,EAAa,CAAC,KAC/C,GAAUzkB,GAAWujB,GAAYG,kBAAkB1jB,GAAW,CAAC,KAC7C,iBAAXqkB,EAAsBA,EAAS,CAAC,EAE/C,CAEAG,iBAAiBH,EAAQM,EAAc3E,KAAK0E,YAAYR,aACtD,IAAK,MAAM3hB,KAAY9E,OAAO4D,KAAKsjB,GAAc,CAC/C,MAAMC,EAAgBD,EAAYpiB,GAC5BnE,EAAQimB,EAAO9hB,GACfsiB,EAAY,GAAUzmB,GAAS,UA1uBrCsc,OADSA,EA2uB+Ctc,GAzuBnD,GAAGsc,IAGLjd,OAAOM,UAAUuC,SAASrC,KAAKyc,GAAQoK,MAAM,eAAe,GAAG5kB,cAwuBlE,IAAK,IAAI6kB,OAAOH,GAAe9gB,KAAK+gB,GAClC,MAAM,IAAIG,UAAU,GAAGhF,KAAK0E,YAAYjI,KAAKwI,0BAA0B1iB,qBAA4BsiB,yBAAiCD,MAExI,CAhvBWlK,KAivBb,EAmBF,MAAMwK,WAAsBlB,GAC1BU,YAAY1kB,EAASqkB,GACnBc,SACAnlB,EAAU6a,GAAW7a,MAMrBggB,KAAKoF,SAAWplB,EAChBggB,KAAKqF,QAAUrF,KAAKoE,WAAWC,GAC/B5B,GAAKjQ,IAAIwN,KAAKoF,SAAUpF,KAAK0E,YAAYY,SAAUtF,MACrD,CAGAuF,UACE9C,GAAKM,OAAO/C,KAAKoF,SAAUpF,KAAK0E,YAAYY,UAC5C/E,GAAaC,IAAIR,KAAKoF,SAAUpF,KAAK0E,YAAYc,WAEjD,IAAK,MAAMC,KAAgBhoB,OAAOioB,oBAAoB1F,MACpDA,KAAKyF,GAAgB,IAEzB,CAEAE,eAAepJ,EAAUvc,EAAS4lB,GAAa,GAC7C5I,GAAuBT,EAAUvc,EAAS4lB,EAC5C,CAEAxB,WAAWC,GAMT,OALAA,EAASrE,KAAKsE,gBAAgBD,EAAQrE,KAAKoF,UAC3Cf,EAASrE,KAAKuE,kBAAkBF,GAEhCrE,KAAKwE,iBAAiBH,GAEfA,CACT,CAGAwB,mBAAmB7lB,GACjB,OAAOyiB,GAAK7kB,IAAIid,GAAW7a,GAAUggB,KAAKsF,SAC5C,CAEAO,2BAA2B7lB,EAASqkB,EAAS,CAAC,GAC5C,OAAOrE,KAAK8F,YAAY9lB,IAAY,IAAIggB,KAAKhgB,EAA2B,iBAAXqkB,EAAsBA,EAAS,KAC9F,CAEW0B,qBACT,MApDY,OAqDd,CAEWT,sBACT,MAAO,MAAMtF,KAAKvD,MACpB,CAEW+I,uBACT,MAAO,IAAIxF,KAAKsF,UAClB,CAEAO,iBAAiB9kB,GACf,MAAO,GAAGA,IAAOif,KAAKwF,WACxB,EAWF,MAAMQ,GAAuB,CAACC,EAAWC,EAAS,UAChD,MAAMC,EAAa,gBAAgBF,EAAUT,YACvCzkB,EAAOklB,EAAUxJ,KACvB8D,GAAaY,GAAGrb,SAAUqgB,EAAY,qBAAqBplB,OAAU,SAAUqe,GAK7E,GAJI,CAAC,IAAK,QAAQlF,SAAS8F,KAAKoG,UAC9BhH,EAAM+C,iBAGJ9G,GAAW2E,MACb,OAGF,MAAMhT,EAASsN,GAAuB0F,OAASA,KAAK7E,QAAQ,IAAIpa,KAC/CklB,EAAUI,oBAAoBrZ,GAEtCkZ,IACX,GAAE,EAeEI,GAAc,YACdC,GAAc,QAAQD,KACtBE,GAAe,SAASF,KAO9B,MAAMG,WAAcvB,GAEPzI,kBACT,MAdW,OAeb,CAGAiK,QAGE,GAFmBnG,GAAakB,QAAQzB,KAAKoF,SAAUmB,IAExC1E,iBACb,OAGF7B,KAAKoF,SAAS5J,UAAUuH,OAnBF,QAqBtB,MAAM6C,EAAa5F,KAAKoF,SAAS5J,UAAUvW,SAtBrB,QAwBtB+a,KAAK2F,gBAAe,IAAM3F,KAAK2G,mBAAmB3G,KAAKoF,SAAUQ,EACnE,CAGAe,kBACE3G,KAAKoF,SAASrC,SAEdxC,GAAakB,QAAQzB,KAAKoF,SAAUoB,IACpCxG,KAAKuF,SACP,CAGAM,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAO2b,GAAMJ,oBAAoBrG,MAEvC,GAAsB,iBAAXqE,EAAX,CAIA,QAAqB7K,IAAjB1O,EAAKuZ,IAAyBA,EAAOlK,WAAW,MAAmB,gBAAXkK,EAC1D,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,GAAQrE,KANb,CAOF,GACF,EAQFgG,GAAqBS,GAAO,SAK5BpK,GAAmBoK,IAYnB,MAKMI,GAAyB,4BAM/B,MAAMC,WAAe5B,GAERzI,kBACT,MAdW,QAeb,CAGAsK,SAEE/G,KAAKoF,SAASvjB,aAAa,eAAgBme,KAAKoF,SAAS5J,UAAUuL,OAhB3C,UAiB1B,CAGAlB,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOgc,GAAOT,oBAAoBrG,MAEzB,WAAXqE,GACFvZ,EAAKuZ,IAET,GACF,EAQF9D,GAAaY,GAAGrb,SAlCe,2BAkCmB+gB,IAAwBzH,IACxEA,EAAM+C,iBACN,MAAM6E,EAAS5H,EAAMpS,OAAOmO,QAAQ0L,IACvBC,GAAOT,oBAAoBW,GACnCD,QAAQ,IAMf1K,GAAmByK,IAYnB,MAAMG,GAAiB,CACrBrU,KAAI,CAACmH,EAAU/Z,EAAU8F,SAASC,kBACzB,GAAG3G,UAAUsB,QAAQ3C,UAAUqiB,iBAAiBniB,KAAK+B,EAAS+Z,IAGvEmN,QAAO,CAACnN,EAAU/Z,EAAU8F,SAASC,kBAC5BrF,QAAQ3C,UAAU8K,cAAc5K,KAAK+B,EAAS+Z,GAGvDoN,SAAQ,CAACnnB,EAAS+Z,IACT,GAAG3a,UAAUY,EAAQmnB,UAAUvgB,QAAOzB,GAASA,EAAMiiB,QAAQrN,KAGtEsN,QAAQrnB,EAAS+Z,GACf,MAAMsN,EAAU,GAChB,IAAIC,EAAWtnB,EAAQwF,WAAW2V,QAAQpB,GAE1C,KAAOuN,GACLD,EAAQhV,KAAKiV,GACbA,EAAWA,EAAS9hB,WAAW2V,QAAQpB,GAGzC,OAAOsN,CACT,EAEAE,KAAKvnB,EAAS+Z,GACZ,IAAIyN,EAAWxnB,EAAQynB,uBAEvB,KAAOD,GAAU,CACf,GAAIA,EAASJ,QAAQrN,GACnB,MAAO,CAACyN,GAGVA,EAAWA,EAASC,sBACtB,CAEA,MAAO,EACT,EAGAniB,KAAKtF,EAAS+Z,GACZ,IAAIzU,EAAOtF,EAAQ0nB,mBAEnB,KAAOpiB,GAAM,CACX,GAAIA,EAAK8hB,QAAQrN,GACf,MAAO,CAACzU,GAGVA,EAAOA,EAAKoiB,kBACd,CAEA,MAAO,EACT,EAEAC,kBAAkB3nB,GAChB,MAAM4nB,EAAa,CAAC,IAAK,SAAU,QAAS,WAAY,SAAU,UAAW,aAAc,4BAA4BrkB,KAAIwW,GAAY,GAAGA,2BAAiCpW,KAAK,KAChL,OAAOqc,KAAKpN,KAAKgV,EAAY5nB,GAAS4G,QAAOihB,IAAOxM,GAAWwM,IAAO/M,GAAU+M,IAClF,GAeIC,GAAc,YACdC,GAAmB,aAAaD,KAChCE,GAAkB,YAAYF,KAC9BG,GAAiB,WAAWH,KAC5BI,GAAoB,cAAcJ,KAClCK,GAAkB,YAAYL,KAK9BM,GAAY,CAChBC,YAAa,KACbC,aAAc,KACdC,cAAe,MAEXC,GAAgB,CACpBH,YAAa,kBACbC,aAAc,kBACdC,cAAe,mBAMjB,MAAME,WAAczE,GAClBU,YAAY1kB,EAASqkB,GACnBc,QACAnF,KAAKoF,SAAWplB,EAEXA,GAAYyoB,GAAMC,gBAIvB1I,KAAKqF,QAAUrF,KAAKoE,WAAWC,GAC/BrE,KAAK2I,QAAU,EACf3I,KAAK4I,sBAAwB9H,QAAQzgB,OAAOwoB,cAE5C7I,KAAK8I,cACP,CAGW7E,qBACT,OAAOmE,EACT,CAEWlE,yBACT,OAAOsE,EACT,CAEW/L,kBACT,MAnDW,OAoDb,CAGA8I,UACEhF,GAAaC,IAAIR,KAAKoF,SAAU0C,GAClC,CAGAiB,OAAO3J,GACAY,KAAK4I,sBAKN5I,KAAKgJ,wBAAwB5J,KAC/BY,KAAK2I,QAAUvJ,EAAM6J,SALrBjJ,KAAK2I,QAAUvJ,EAAM8J,QAAQ,GAAGD,OAOpC,CAEAE,KAAK/J,GACCY,KAAKgJ,wBAAwB5J,KAC/BY,KAAK2I,QAAUvJ,EAAM6J,QAAUjJ,KAAK2I,SAGtC3I,KAAKoJ,eAELrM,GAAQiD,KAAKqF,QAAQgD,YACvB,CAEAgB,MAAMjK,GACJY,KAAK2I,QAAUvJ,EAAM8J,SAAW9J,EAAM8J,QAAQ/X,OAAS,EAAI,EAAIiO,EAAM8J,QAAQ,GAAGD,QAAUjJ,KAAK2I,OACjG,CAEAS,eACE,MAAME,EAAY1mB,KAAKoC,IAAIgb,KAAK2I,SAEhC,GAAIW,GA9EgB,GA+ElB,OAGF,MAAMvb,EAAYub,EAAYtJ,KAAK2I,QACnC3I,KAAK2I,QAAU,EAEV5a,GAILgP,GAAQhP,EAAY,EAAIiS,KAAKqF,QAAQkD,cAAgBvI,KAAKqF,QAAQiD,aACpE,CAEAQ,cACM9I,KAAK4I,uBACPrI,GAAaY,GAAGnB,KAAKoF,SAAU8C,IAAmB9I,GAASY,KAAK+I,OAAO3J,KACvEmB,GAAaY,GAAGnB,KAAKoF,SAAU+C,IAAiB/I,GAASY,KAAKmJ,KAAK/J,KAEnEY,KAAKoF,SAAS5J,UAAUtE,IAlGG,mBAoG3BqJ,GAAaY,GAAGnB,KAAKoF,SAAU2C,IAAkB3I,GAASY,KAAK+I,OAAO3J,KACtEmB,GAAaY,GAAGnB,KAAKoF,SAAU4C,IAAiB5I,GAASY,KAAKqJ,MAAMjK,KACpEmB,GAAaY,GAAGnB,KAAKoF,SAAU6C,IAAgB7I,GAASY,KAAKmJ,KAAK/J,KAEtE,CAEA4J,wBAAwB5J,GACtB,OAAOY,KAAK4I,wBA5GS,QA4GiBxJ,EAAMmK,aA7GrB,UA6GyDnK,EAAMmK,YACxF,CAGA1D,qBACE,MAAO,iBAAkB/f,SAASC,iBAAmB7C,UAAUsmB,eAAiB,CAClF,EAcF,MAEMC,GAAc,eACdC,GAAiB,YAKjBC,GAAa,OACbC,GAAa,OACbC,GAAiB,OACjBC,GAAkB,QAClBC,GAAc,QAAQN,KACtBO,GAAa,OAAOP,KACpBQ,GAAkB,UAAUR,KAC5BS,GAAqB,aAAaT,KAClCU,GAAqB,aAAaV,KAClCW,GAAmB,YAAYX,KAC/BY,GAAwB,OAAOZ,KAAcC,KAC7CY,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAsB,WACtBC,GAAsB,SAMtBC,GAAkB,UAClBC,GAAgB,iBAChBC,GAAuBF,GAAkBC,GAKzCE,GAAmB,CACvB,UAAoBd,GACpB,WAAqBD,IAEjBgB,GAAY,CAChBC,SAAU,IACVC,UAAU,EACVC,MAAO,QACPC,MAAM,EACNC,OAAO,EACPC,MAAM,GAEFC,GAAgB,CACpBN,SAAU,mBAEVC,SAAU,UACVC,MAAO,mBACPC,KAAM,mBACNC,MAAO,UACPC,KAAM,WAMR,MAAME,WAAiBnG,GACrBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAKsL,UAAY,KACjBtL,KAAKuL,eAAiB,KACtBvL,KAAKwL,YAAa,EAClBxL,KAAKyL,aAAe,KACpBzL,KAAK0L,aAAe,KACpB1L,KAAK2L,mBAAqB1E,GAAeC,QApCjB,uBAoC8ClH,KAAKoF,UAE3EpF,KAAK4L,qBAED5L,KAAKqF,QAAQ4F,OAASV,IACxBvK,KAAK6L,OAET,CAGW5H,qBACT,OAAO4G,EACT,CAEW3G,yBACT,OAAOkH,EACT,CAEW3O,kBACT,MAtFW,UAuFb,CAGAnX,OACE0a,KAAK8L,OAAOnC,GACd,CAEAoC,mBAIOjmB,SAASkmB,QAAUlR,GAAUkF,KAAKoF,WACrCpF,KAAK1a,MAET,CAEAiiB,OACEvH,KAAK8L,OAAOlC,GACd,CAEAoB,QACMhL,KAAKwL,YACPjR,GAAqByF,KAAKoF,UAG5BpF,KAAKiM,gBACP,CAEAJ,QACE7L,KAAKiM,iBAELjM,KAAKkM,kBAELlM,KAAKsL,UAAYa,aAAY,IAAMnM,KAAK+L,mBAAmB/L,KAAKqF,QAAQyF,SAC1E,CAEAsB,oBACOpM,KAAKqF,QAAQ4F,OAIdjL,KAAKwL,WACPjL,GAAaa,IAAIpB,KAAKoF,SAAU4E,IAAY,IAAMhK,KAAK6L,UAIzD7L,KAAK6L,QACP,CAEAQ,GAAGnT,GACD,MAAMoT,EAAQtM,KAAKuM,YAEnB,GAAIrT,EAAQoT,EAAMnb,OAAS,GAAK+H,EAAQ,EACtC,OAGF,GAAI8G,KAAKwL,WAEP,YADAjL,GAAaa,IAAIpB,KAAKoF,SAAU4E,IAAY,IAAMhK,KAAKqM,GAAGnT,KAI5D,MAAMsT,EAAcxM,KAAKyM,cAAczM,KAAK0M,cAE5C,GAAIF,IAAgBtT,EAClB,OAGF,MAAMtC,EAAQsC,EAAQsT,EAAc7C,GAAaC,GAEjD5J,KAAK8L,OAAOlV,EAAO0V,EAAMpT,GAC3B,CAEAqM,UACMvF,KAAK0L,cACP1L,KAAK0L,aAAanG,UAGpBJ,MAAMI,SACR,CAGAhB,kBAAkBF,GAEhB,OADAA,EAAOsI,gBAAkBtI,EAAOyG,SACzBzG,CACT,CAEAuH,qBACM5L,KAAKqF,QAAQ0F,UACfxK,GAAaY,GAAGnB,KAAKoF,SAAU6E,IAAiB7K,GAASY,KAAK4M,SAASxN,KAG9C,UAAvBY,KAAKqF,QAAQ2F,QACfzK,GAAaY,GAAGnB,KAAKoF,SAAU8E,IAAoB,IAAMlK,KAAKgL,UAC9DzK,GAAaY,GAAGnB,KAAKoF,SAAU+E,IAAoB,IAAMnK,KAAKoM,uBAG5DpM,KAAKqF,QAAQ6F,OAASzC,GAAMC,eAC9B1I,KAAK6M,yBAET,CAEAA,0BACE,IAAK,MAAMC,KAAO7F,GAAerU,KA/JX,qBA+JmCoN,KAAKoF,UAC5D7E,GAAaY,GAAG2L,EAAK1C,IAAkBhL,GAASA,EAAM+C,mBAGxD,MAqBM4K,EAAc,CAClBzE,aAAc,IAAMtI,KAAK8L,OAAO9L,KAAKgN,kBAAkBnD,KACvDtB,cAAe,IAAMvI,KAAK8L,OAAO9L,KAAKgN,kBAAkBlD,KACxDzB,YAxBkB,KACS,UAAvBrI,KAAKqF,QAAQ2F,QAWjBhL,KAAKgL,QAEDhL,KAAKyL,cACPwB,aAAajN,KAAKyL,cAGpBzL,KAAKyL,aAAe5N,YAAW,IAAMmC,KAAKoM,qBA7MjB,IA6M+DpM,KAAKqF,QAAQyF,UAAS,GAQhH9K,KAAK0L,aAAe,IAAIjD,GAAMzI,KAAKoF,SAAU2H,EAC/C,CAEAH,SAASxN,GACP,GAAI,kBAAkBtb,KAAKsb,EAAMpS,OAAOoZ,SACtC,OAGF,MAAMrY,EAAY6c,GAAiBxL,EAAM7hB,KAErCwQ,IACFqR,EAAM+C,iBAENnC,KAAK8L,OAAO9L,KAAKgN,kBAAkBjf,IAEvC,CAEA0e,cAAczsB,GACZ,OAAOggB,KAAKuM,YAAY3mB,QAAQ5F,EAClC,CAEAktB,2BAA2BhU,GACzB,IAAK8G,KAAK2L,mBACR,OAGF,MAAMwB,EAAkBlG,GAAeC,QAAQuD,GAAiBzK,KAAK2L,oBACrEwB,EAAgB3R,UAAUuH,OAAOyH,IACjC2C,EAAgBvrB,gBAAgB,gBAChC,MAAMwrB,EAAqBnG,GAAeC,QAAQ,sBAAsBhO,MAAW8G,KAAK2L,oBAEpFyB,IACFA,EAAmB5R,UAAUtE,IAAIsT,IACjC4C,EAAmBvrB,aAAa,eAAgB,QAEpD,CAEAqqB,kBACE,MAAMlsB,EAAUggB,KAAKuL,gBAAkBvL,KAAK0M,aAE5C,IAAK1sB,EACH,OAGF,MAAMqtB,EAAkB9P,OAAO+P,SAASttB,EAAQga,aAAa,oBAAqB,IAClFgG,KAAKqF,QAAQyF,SAAWuC,GAAmBrN,KAAKqF,QAAQsH,eAC1D,CAEAb,OAAOlV,EAAO5W,EAAU,MACtB,GAAIggB,KAAKwL,WACP,OAGF,MAAMzN,EAAgBiC,KAAK0M,aAErBa,EAAS3W,IAAU+S,GACnB6D,EAAcxtB,GAAW8d,GAAqBkC,KAAKuM,YAAaxO,EAAewP,EAAQvN,KAAKqF,QAAQ8F,MAE1G,GAAIqC,IAAgBzP,EAClB,OAGF,MAAM0P,EAAmBzN,KAAKyM,cAAce,GAEtCE,EAAeC,GACZpN,GAAakB,QAAQzB,KAAKoF,SAAUuI,EAAW,CACpD7N,cAAe0N,EACfzf,UAAWiS,KAAK4N,kBAAkBhX,GAClCkM,KAAM9C,KAAKyM,cAAc1O,GACzBsO,GAAIoB,IAMR,GAFmBC,EAAa3D,IAEjBlI,iBACb,OAGF,IAAK9D,IAAkByP,EAGrB,OAGF,MAAMK,EAAY/M,QAAQd,KAAKsL,WAC/BtL,KAAKgL,QACLhL,KAAKwL,YAAa,EAElBxL,KAAKkN,2BAA2BO,GAEhCzN,KAAKuL,eAAiBiC,EACtB,MAAMM,EAAuBP,EA/RR,sBADF,oBAiSbQ,EAAiBR,EA/RH,qBACA,qBA+RpBC,EAAYhS,UAAUtE,IAAI6W,GAC1BhS,GAAOyR,GACPzP,EAAcvC,UAAUtE,IAAI4W,GAC5BN,EAAYhS,UAAUtE,IAAI4W,GAU1B9N,KAAK2F,gBARoB,KACvB6H,EAAYhS,UAAUuH,OAAO+K,EAAsBC,GACnDP,EAAYhS,UAAUtE,IAAIsT,IAC1BzM,EAAcvC,UAAUuH,OAAOyH,GAAqBuD,EAAgBD,GACpE9N,KAAKwL,YAAa,EAClBkC,EAAa1D,GAAW,GAGYjM,EAAeiC,KAAKgO,eAEtDH,GACF7N,KAAK6L,OAET,CAEAmC,cACE,OAAOhO,KAAKoF,SAAS5J,UAAUvW,SAxTV,QAyTvB,CAEAynB,aACE,OAAOzF,GAAeC,QAAQyD,GAAsB3K,KAAKoF,SAC3D,CAEAmH,YACE,OAAOtF,GAAerU,KAAK8X,GAAe1K,KAAKoF,SACjD,CAEA6G,iBACMjM,KAAKsL,YACP2C,cAAcjO,KAAKsL,WACnBtL,KAAKsL,UAAY,KAErB,CAEA0B,kBAAkBjf,GAChB,OAAIoO,KACKpO,IAAc8b,GAAiBD,GAAaD,GAG9C5b,IAAc8b,GAAiBF,GAAaC,EACrD,CAEAgE,kBAAkBhX,GAChB,OAAIuF,KACKvF,IAAUgT,GAAaC,GAAiBC,GAG1ClT,IAAUgT,GAAaE,GAAkBD,EAClD,CAGAhE,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOugB,GAAShF,oBAAoBrG,KAAMqE,GAEhD,GAAsB,iBAAXA,GAKX,GAAsB,iBAAXA,EAAqB,CAC9B,QAAqB7K,IAAjB1O,EAAKuZ,IAAyBA,EAAOlK,WAAW,MAAmB,gBAAXkK,EAC1D,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IACP,OAVEvZ,EAAKuhB,GAAGhI,EAWZ,GACF,EAQF9D,GAAaY,GAAGrb,SAAUwkB,GA1WE,uCA0W2C,SAAUlL,GAC/E,MAAMpS,EAASsN,GAAuB0F,MAEtC,IAAKhT,IAAWA,EAAOwO,UAAUvW,SAASslB,IACxC,OAGFnL,EAAM+C,iBACN,MAAM+L,EAAW7C,GAAShF,oBAAoBrZ,GACxCmhB,EAAanO,KAAKhG,aAAa,oBAErC,OAAImU,GACFD,EAAS7B,GAAG8B,QAEZD,EAAS9B,qBAKyC,SAAhD7I,GAAYQ,iBAAiB/D,KAAM,UACrCkO,EAAS5oB,YAET4oB,EAAS9B,sBAKX8B,EAAS3G,YAET2G,EAAS9B,oBACX,IACA7L,GAAaY,GAAG9gB,OAAQgqB,IAAuB,KAC7C,MAAM+D,EAAYnH,GAAerU,KAzYR,6BA2YzB,IAAK,MAAMsb,KAAYE,EACrB/C,GAAShF,oBAAoB6H,EAC/B,IAMF7R,GAAmBgP,IAYnB,MAEMgD,GAAc,eAEdC,GAAe,OAAOD,KACtBE,GAAgB,QAAQF,KACxBG,GAAe,OAAOH,KACtBI,GAAiB,SAASJ,KAC1BK,GAAyB,QAAQL,cACjCM,GAAoB,OACpBC,GAAsB,WACtBC,GAAwB,aAExBC,GAA6B,WAAWF,OAAwBA,KAKhEG,GAAyB,8BACzBC,GAAY,CAChB9pB,OAAQ,KACR6hB,QAAQ,GAEJkI,GAAgB,CACpB/pB,OAAQ,iBACR6hB,OAAQ,WAMV,MAAMmI,WAAiBhK,GACrBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAKmP,kBAAmB,EACxBnP,KAAKoP,cAAgB,GACrB,MAAMC,EAAapI,GAAerU,KAAKmc,IAEvC,IAAK,MAAMO,KAAQD,EAAY,CAC7B,MAAMtV,EAAWM,GAAuBiV,GAClCC,EAAgBtI,GAAerU,KAAKmH,GAAUnT,QAAO4oB,GAAgBA,IAAiBxP,KAAKoF,WAEhF,OAAbrL,GAAqBwV,EAAcpe,QACrC6O,KAAKoP,cAAc/c,KAAKid,EAE5B,CAEAtP,KAAKyP,sBAEAzP,KAAKqF,QAAQngB,QAChB8a,KAAK0P,0BAA0B1P,KAAKoP,cAAepP,KAAK2P,YAGtD3P,KAAKqF,QAAQ0B,QACf/G,KAAK+G,QAET,CAGW9C,qBACT,OAAO+K,EACT,CAEW9K,yBACT,OAAO+K,EACT,CAEWxS,kBACT,MApEW,UAqEb,CAGAsK,SACM/G,KAAK2P,WACP3P,KAAK4P,OAEL5P,KAAK6P,MAET,CAEAA,OACE,GAAI7P,KAAKmP,kBAAoBnP,KAAK2P,WAChC,OAGF,IAAIG,EAAiB,GAQrB,GANI9P,KAAKqF,QAAQngB,SACf4qB,EAAiB9P,KAAK+P,uBAvEH,wCAuE4CnpB,QAAO5G,GAAWA,IAAYggB,KAAKoF,WAAU7hB,KAAIvD,GAAWkvB,GAAS7I,oBAAoBrmB,EAAS,CAC/J+mB,QAAQ,OAIR+I,EAAe3e,QAAU2e,EAAe,GAAGX,iBAC7C,OAKF,GAFmB5O,GAAakB,QAAQzB,KAAKoF,SAAUkJ,IAExCzM,iBACb,OAGF,IAAK,MAAMmO,KAAkBF,EAC3BE,EAAeJ,OAGjB,MAAMK,EAAYjQ,KAAKkQ,gBAEvBlQ,KAAKoF,SAAS5J,UAAUuH,OAAO6L,IAE/B5O,KAAKoF,SAAS5J,UAAUtE,IAAI2X,IAE5B7O,KAAKoF,SAAS5jB,MAAMyuB,GAAa,EAEjCjQ,KAAK0P,0BAA0B1P,KAAKoP,eAAe,GAEnDpP,KAAKmP,kBAAmB,EAExB,MAYMgB,EAAa,SADUF,EAAU,GAAGhL,cAAgBgL,EAAUpd,MAAM,KAG1EmN,KAAK2F,gBAdY,KACf3F,KAAKmP,kBAAmB,EAExBnP,KAAKoF,SAAS5J,UAAUuH,OAAO8L,IAE/B7O,KAAKoF,SAAS5J,UAAUtE,IAAI0X,GAAqBD,IAEjD3O,KAAKoF,SAAS5jB,MAAMyuB,GAAa,GACjC1P,GAAakB,QAAQzB,KAAKoF,SAAUmJ,GAAc,GAMtBvO,KAAKoF,UAAU,GAE7CpF,KAAKoF,SAAS5jB,MAAMyuB,GAAa,GAAGjQ,KAAKoF,SAAS+K,MACpD,CAEAP,OACE,GAAI5P,KAAKmP,mBAAqBnP,KAAK2P,WACjC,OAKF,GAFmBpP,GAAakB,QAAQzB,KAAKoF,SAAUoJ,IAExC3M,iBACb,OAGF,MAAMoO,EAAYjQ,KAAKkQ,gBAEvBlQ,KAAKoF,SAAS5jB,MAAMyuB,GAAa,GAAGjQ,KAAKoF,SAASrhB,wBAAwBksB,OAC1ElU,GAAOiE,KAAKoF,UAEZpF,KAAKoF,SAAS5J,UAAUtE,IAAI2X,IAE5B7O,KAAKoF,SAAS5J,UAAUuH,OAAO6L,GAAqBD,IAEpD,IAAK,MAAMlN,KAAWzB,KAAKoP,cAAe,CACxC,MAAMpvB,EAAUsa,GAAuBmH,GAEnCzhB,IAAYggB,KAAK2P,SAAS3vB,IAC5BggB,KAAK0P,0BAA0B,CAACjO,IAAU,EAE9C,CAEAzB,KAAKmP,kBAAmB,EAYxBnP,KAAKoF,SAAS5jB,MAAMyuB,GAAa,GAEjCjQ,KAAK2F,gBAZY,KACf3F,KAAKmP,kBAAmB,EAExBnP,KAAKoF,SAAS5J,UAAUuH,OAAO8L,IAE/B7O,KAAKoF,SAAS5J,UAAUtE,IAAI0X,IAE5BrO,GAAakB,QAAQzB,KAAKoF,SAAUqJ,GAAe,GAKvBzO,KAAKoF,UAAU,EAC/C,CAEAuK,SAAS3vB,EAAUggB,KAAKoF,UACtB,OAAOplB,EAAQwb,UAAUvW,SAAS0pB,GACpC,CAGApK,kBAAkBF,GAIhB,OAHAA,EAAO0C,OAASjG,QAAQuD,EAAO0C,QAE/B1C,EAAOnf,OAAS2V,GAAWwJ,EAAOnf,QAC3Bmf,CACT,CAEA6L,gBACE,OAAOlQ,KAAKoF,SAAS5J,UAAUvW,SAtLL,uBAChB,QACC,QAqLb,CAEAwqB,sBACE,IAAKzP,KAAKqF,QAAQngB,OAChB,OAGF,MAAMiiB,EAAWnH,KAAK+P,uBAAuBhB,IAE7C,IAAK,MAAM/uB,KAAWmnB,EAAU,CAC9B,MAAMiJ,EAAW9V,GAAuBta,GAEpCowB,GACFpQ,KAAK0P,0BAA0B,CAAC1vB,GAAUggB,KAAK2P,SAASS,GAE5D,CACF,CAEAL,uBAAuBhW,GACrB,MAAMoN,EAAWF,GAAerU,KAAKkc,GAA4B9O,KAAKqF,QAAQngB,QAE9E,OAAO+hB,GAAerU,KAAKmH,EAAUiG,KAAKqF,QAAQngB,QAAQ0B,QAAO5G,IAAYmnB,EAASjN,SAASla,IACjG,CAEA0vB,0BAA0BW,EAAcC,GACtC,GAAKD,EAAalf,OAIlB,IAAK,MAAMnR,KAAWqwB,EACpBrwB,EAAQwb,UAAUuL,OAvNK,aAuNyBuJ,GAChDtwB,EAAQ6B,aAAa,gBAAiByuB,EAE1C,CAGAzK,uBAAuBxB,GACrB,MAAMgB,EAAU,CAAC,EAMjB,MAJsB,iBAAXhB,GAAuB,YAAYvgB,KAAKugB,KACjDgB,EAAQ0B,QAAS,GAGZ/G,KAAK4G,MAAK,WACf,MAAM9b,EAAOokB,GAAS7I,oBAAoBrG,KAAMqF,GAEhD,GAAsB,iBAAXhB,EAAqB,CAC9B,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IACP,CACF,GACF,EAQF9D,GAAaY,GAAGrb,SAAU4oB,GAAwBK,IAAwB,SAAU3P,IAErD,MAAzBA,EAAMpS,OAAOoZ,SAAmBhH,EAAMW,gBAAmD,MAAjCX,EAAMW,eAAeqG,UAC/EhH,EAAM+C,iBAGR,MAAMpI,EAAWM,GAAuB2F,MAClCuQ,EAAmBtJ,GAAerU,KAAKmH,GAE7C,IAAK,MAAM/Z,KAAWuwB,EACpBrB,GAAS7I,oBAAoBrmB,EAAS,CACpC+mB,QAAQ,IACPA,QAEP,IAKA1K,GAAmB6S,IAYnB,MAAMsB,GAAS,WAETC,GAAc,eACdC,GAAiB,YAGjBC,GAAiB,UACjBC,GAAmB,YAGnBC,GAAe,OAAOJ,KACtBK,GAAiB,SAASL,KAC1BM,GAAe,OAAON,KACtBO,GAAgB,QAAQP,KACxBQ,GAAyB,QAAQR,KAAcC,KAC/CQ,GAAyB,UAAUT,KAAcC,KACjDS,GAAuB,QAAQV,KAAcC,KAC7CU,GAAoB,OAMpBC,GAAyB,4DACzBC,GAA6B,GAAGD,MAA0BD,KAC1DG,GAAgB,iBAIhBC,GAAgBrV,KAAU,UAAY,YACtCsV,GAAmBtV,KAAU,YAAc,UAC3CuV,GAAmBvV,KAAU,aAAe,eAC5CwV,GAAsBxV,KAAU,eAAiB,aACjDyV,GAAkBzV,KAAU,aAAe,cAC3C0V,GAAiB1V,KAAU,cAAgB,aAG3C2V,GAAY,CAChBC,WAAW,EACXrjB,SAAU,kBACVsjB,QAAS,UACTvpB,OAAQ,CAAC,EAAG,GACZwpB,aAAc,KACdlzB,UAAW,UAEPmzB,GAAgB,CACpBH,UAAW,mBACXrjB,SAAU,mBACVsjB,QAAS,SACTvpB,OAAQ,0BACRwpB,aAAc,yBACdlzB,UAAW,2BAMb,MAAMozB,WAAiBjN,GACrBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAKoS,QAAU,KACfpS,KAAKqS,QAAUrS,KAAKoF,SAAS5f,WAG7Bwa,KAAKsS,MAAQrL,GAAe3hB,KAAK0a,KAAKoF,SAAUmM,IAAe,IAAMtK,GAAeM,KAAKvH,KAAKoF,SAAUmM,IAAe,IAAMtK,GAAeC,QAAQqK,GAAevR,KAAKqS,SACxKrS,KAAKuS,UAAYvS,KAAKwS,eACxB,CAGWvO,qBACT,OAAO6N,EACT,CAEW5N,yBACT,OAAOgO,EACT,CAEWzV,kBACT,OAAO+T,EACT,CAGAzJ,SACE,OAAO/G,KAAK2P,WAAa3P,KAAK4P,OAAS5P,KAAK6P,MAC9C,CAEAA,OACE,GAAIxU,GAAW2E,KAAKoF,WAAapF,KAAK2P,WACpC,OAGF,MAAM7P,EAAgB,CACpBA,cAAeE,KAAKoF,UAItB,IAFkB7E,GAAakB,QAAQzB,KAAKoF,SAAU2L,GAAcjR,GAEtD+B,iBAAd,CAUA,GANA7B,KAAKyS,gBAMD,iBAAkB3sB,SAASC,kBAAoBia,KAAKqS,QAAQlX,QA/ExC,eAgFtB,IAAK,MAAMnb,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAKwa,UAC/C5G,GAAaY,GAAGnhB,EAAS,YAAa8b,IAI1CkE,KAAKoF,SAASsN,QAEd1S,KAAKoF,SAASvjB,aAAa,iBAAiB,GAE5Cme,KAAKsS,MAAM9W,UAAUtE,IAAIka,IAEzBpR,KAAKoF,SAAS5J,UAAUtE,IAAIka,IAE5B7Q,GAAakB,QAAQzB,KAAKoF,SAAU4L,GAAelR,EAtBnD,CAuBF,CAEA8P,OACE,GAAIvU,GAAW2E,KAAKoF,YAAcpF,KAAK2P,WACrC,OAGF,MAAM7P,EAAgB,CACpBA,cAAeE,KAAKoF,UAGtBpF,KAAK2S,cAAc7S,EACrB,CAEAyF,UACMvF,KAAKoS,SACPpS,KAAKoS,QAAQ3Y,UAGf0L,MAAMI,SACR,CAEA/Z,SACEwU,KAAKuS,UAAYvS,KAAKwS,gBAElBxS,KAAKoS,SACPpS,KAAKoS,QAAQ5mB,QAEjB,CAGAmnB,cAAc7S,GAGZ,IAFkBS,GAAakB,QAAQzB,KAAKoF,SAAUyL,GAAc/Q,GAEtD+B,iBAAd,CAMA,GAAI,iBAAkB/b,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAKwa,UAC/C5G,GAAaC,IAAIxgB,EAAS,YAAa8b,IAIvCkE,KAAKoS,SACPpS,KAAKoS,QAAQ3Y,UAGfuG,KAAKsS,MAAM9W,UAAUuH,OAAOqO,IAE5BpR,KAAKoF,SAAS5J,UAAUuH,OAAOqO,IAE/BpR,KAAKoF,SAASvjB,aAAa,gBAAiB,SAE5C0hB,GAAYE,oBAAoBzD,KAAKsS,MAAO,UAC5C/R,GAAakB,QAAQzB,KAAKoF,SAAU0L,GAAgBhR,EArBpD,CAsBF,CAEAsE,WAAWC,GAGT,GAAgC,iBAFhCA,EAASc,MAAMf,WAAWC,IAERtlB,YAA2B,GAAUslB,EAAOtlB,YAAgE,mBAA3CslB,EAAOtlB,UAAUgF,sBAElG,MAAM,IAAIihB,UAAU,GAAGwL,GAAOvL,+GAGhC,OAAOZ,CACT,CAEAoO,gBACE,QAAsB,IAAX,EACT,MAAM,IAAIzN,UAAU,gEAGtB,IAAI4N,EAAmB5S,KAAKoF,SAEG,WAA3BpF,KAAKqF,QAAQtmB,UACf6zB,EAAmB5S,KAAKqS,QACf,GAAUrS,KAAKqF,QAAQtmB,WAChC6zB,EAAmB/X,GAAWmF,KAAKqF,QAAQtmB,WACA,iBAA3BihB,KAAKqF,QAAQtmB,YAC7B6zB,EAAmB5S,KAAKqF,QAAQtmB,WAGlC,MAAMkzB,EAAejS,KAAK6S,mBAE1B7S,KAAKoS,QAAU,GAAoBQ,EAAkB5S,KAAKsS,MAAOL,EACnE,CAEAtC,WACE,OAAO3P,KAAKsS,MAAM9W,UAAUvW,SAASmsB,GACvC,CAEA0B,gBACE,MAAMC,EAAiB/S,KAAKqS,QAE5B,GAAIU,EAAevX,UAAUvW,SAxMN,WAyMrB,OAAO2sB,GAGT,GAAImB,EAAevX,UAAUvW,SA3MJ,aA4MvB,OAAO4sB,GAGT,GAAIkB,EAAevX,UAAUvW,SA9MA,iBA+M3B,MAjMsB,MAoMxB,GAAI8tB,EAAevX,UAAUvW,SAjNE,mBAkN7B,MApMyB,SAwM3B,MAAM+tB,EAAkF,QAA1EttB,iBAAiBsa,KAAKsS,OAAOrX,iBAAiB,iBAAiBb,OAE7E,OAAI2Y,EAAevX,UAAUvW,SA5NP,UA6Nb+tB,EAAQvB,GAAmBD,GAG7BwB,EAAQrB,GAAsBD,EACvC,CAEAc,gBACE,OAAkD,OAA3CxS,KAAKoF,SAASjK,QA5ND,UA6NtB,CAEA8X,aACE,MAAM,OACJxqB,GACEuX,KAAKqF,QAET,MAAsB,iBAAX5c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAASmf,OAAO+P,SAASlvB,EAAO,MAGzC,mBAAXqK,EACFyqB,GAAczqB,EAAOyqB,EAAYlT,KAAKoF,UAGxC3c,CACT,CAEAoqB,mBACE,MAAMM,EAAwB,CAC5Bh0B,UAAW6gB,KAAK8S,gBAChBjc,UAAW,CAAC,CACV9V,KAAM,kBACNmB,QAAS,CACPwM,SAAUsR,KAAKqF,QAAQ3W,WAExB,CACD3N,KAAM,SACNmB,QAAS,CACPuG,OAAQuX,KAAKiT,iBAcnB,OATIjT,KAAKuS,WAAsC,WAAzBvS,KAAKqF,QAAQ2M,WACjCzO,GAAYC,iBAAiBxD,KAAKsS,MAAO,SAAU,UAEnDa,EAAsBtc,UAAY,CAAC,CACjC9V,KAAM,cACNC,SAAS,KAIN,IAAKmyB,KAC+B,mBAA9BnT,KAAKqF,QAAQ4M,aAA8BjS,KAAKqF,QAAQ4M,aAAakB,GAAyBnT,KAAKqF,QAAQ4M,aAE1H,CAEAmB,iBAAgB,IACd71B,EAAG,OACHyP,IAEA,MAAMsf,EAAQrF,GAAerU,KA/QF,8DA+Q+BoN,KAAKsS,OAAO1rB,QAAO5G,GAAW8a,GAAU9a,KAE7FssB,EAAMnb,QAMX2M,GAAqBwO,EAAOtf,EAAQzP,IAAQqzB,IAAmBtE,EAAMpS,SAASlN,IAAS0lB,OACzF,CAGA7M,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOqnB,GAAS9L,oBAAoBrG,KAAMqE,GAEhD,GAAsB,iBAAXA,EAAX,CAIA,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IANL,CAOF,GACF,CAEAwB,kBAAkBzG,GAChB,GAhUuB,IAgUnBA,EAAM4H,QAAgD,UAAf5H,EAAMqB,MAnUnC,QAmUuDrB,EAAM7hB,IACzE,OAGF,MAAM81B,EAAcpM,GAAerU,KAAK0e,IAExC,IAAK,MAAMvK,KAAUsM,EAAa,CAChC,MAAMC,EAAUnB,GAASrM,YAAYiB,GAErC,IAAKuM,IAAyC,IAA9BA,EAAQjO,QAAQ0M,UAC9B,SAGF,MAAMwB,EAAenU,EAAMmU,eACrBC,EAAeD,EAAarZ,SAASoZ,EAAQhB,OAEnD,GAAIiB,EAAarZ,SAASoZ,EAAQlO,WAA2C,WAA9BkO,EAAQjO,QAAQ0M,YAA2ByB,GAA8C,YAA9BF,EAAQjO,QAAQ0M,WAA2ByB,EACnJ,SAIF,GAAIF,EAAQhB,MAAMrtB,SAASma,EAAMpS,UAA2B,UAAfoS,EAAMqB,MAxVvC,QAwV2DrB,EAAM7hB,KAAqB,qCAAqCuG,KAAKsb,EAAMpS,OAAOoZ,UACvJ,SAGF,MAAMtG,EAAgB,CACpBA,cAAewT,EAAQlO,UAGN,UAAfhG,EAAMqB,OACRX,EAAcqG,WAAa/G,GAG7BkU,EAAQX,cAAc7S,EACxB,CACF,CAEA+F,6BAA6BzG,GAG3B,MAAMqU,EAAU,kBAAkB3vB,KAAKsb,EAAMpS,OAAOoZ,SAC9CsN,EA7WW,WA6WKtU,EAAM7hB,IACtBo2B,EAAkB,CAAChD,GAAgBC,IAAkB1W,SAASkF,EAAM7hB,KAE1E,IAAKo2B,IAAoBD,EACvB,OAGF,GAAID,IAAYC,EACd,OAGFtU,EAAM+C,iBAEN,MAAMyR,EAAkB5T,KAAKoH,QAAQiK,IAA0BrR,KAAOiH,GAAeM,KAAKvH,KAAMqR,IAAwB,IAAMpK,GAAe3hB,KAAK0a,KAAMqR,IAAwB,IAAMpK,GAAeC,QAAQmK,GAAwBjS,EAAMW,eAAeva,YACpPwF,EAAWmnB,GAAS9L,oBAAoBuN,GAE9C,GAAID,EAMF,OALAvU,EAAMyU,kBACN7oB,EAAS6kB,YAET7kB,EAASooB,gBAAgBhU,GAKvBpU,EAAS2kB,aAEXvQ,EAAMyU,kBACN7oB,EAAS4kB,OACTgE,EAAgBlB,QAEpB,EAQFnS,GAAaY,GAAGrb,SAAUorB,GAAwBG,GAAwBc,GAAS2B,uBACnFvT,GAAaY,GAAGrb,SAAUorB,GAAwBK,GAAeY,GAAS2B,uBAC1EvT,GAAaY,GAAGrb,SAAUmrB,GAAwBkB,GAAS4B,YAC3DxT,GAAaY,GAAGrb,SAAUqrB,GAAsBgB,GAAS4B,YACzDxT,GAAaY,GAAGrb,SAAUmrB,GAAwBI,IAAwB,SAAUjS,GAClFA,EAAM+C,iBACNgQ,GAAS9L,oBAAoBrG,MAAM+G,QACrC,IAKA1K,GAAmB8V,IAYnB,MAAM6B,GAAyB,oDACzBC,GAA0B,cAC1BC,GAAmB,gBACnBC,GAAkB,eAKxB,MAAMC,GACJ1P,cACE1E,KAAKoF,SAAWtf,SAAS6G,IAC3B,CAGA0nB,WAEE,MAAMC,EAAgBxuB,SAASC,gBAAgBuC,YAC/C,OAAO1F,KAAKoC,IAAI3E,OAAOk0B,WAAaD,EACtC,CAEA1E,OACE,MAAMtrB,EAAQ0b,KAAKqU,WAEnBrU,KAAKwU,mBAGLxU,KAAKyU,sBAAsBzU,KAAKoF,SAAU8O,IAAkBQ,GAAmBA,EAAkBpwB,IAGjG0b,KAAKyU,sBAAsBT,GAAwBE,IAAkBQ,GAAmBA,EAAkBpwB,IAE1G0b,KAAKyU,sBAAsBR,GAAyBE,IAAiBO,GAAmBA,EAAkBpwB,GAC5G,CAEAwO,QACEkN,KAAK2U,wBAAwB3U,KAAKoF,SAAU,YAE5CpF,KAAK2U,wBAAwB3U,KAAKoF,SAAU8O,IAE5ClU,KAAK2U,wBAAwBX,GAAwBE,IAErDlU,KAAK2U,wBAAwBV,GAAyBE,GACxD,CAEAS,gBACE,OAAO5U,KAAKqU,WAAa,CAC3B,CAGAG,mBACExU,KAAK6U,sBAAsB7U,KAAKoF,SAAU,YAE1CpF,KAAKoF,SAAS5jB,MAAM+K,SAAW,QACjC,CAEAkoB,sBAAsB1a,EAAU+a,EAAevY,GAC7C,MAAMwY,EAAiB/U,KAAKqU,WAa5BrU,KAAKgV,2BAA2Bjb,GAXH/Z,IAC3B,GAAIA,IAAYggB,KAAKoF,UAAY/kB,OAAOk0B,WAAav0B,EAAQsI,YAAcysB,EACzE,OAGF/U,KAAK6U,sBAAsB70B,EAAS80B,GAEpC,MAAMJ,EAAkBr0B,OAAOqF,iBAAiB1F,GAASib,iBAAiB6Z,GAC1E90B,EAAQwB,MAAMyzB,YAAYH,EAAe,GAAGvY,EAASgB,OAAOC,WAAWkX,QAAsB,GAIjG,CAEAG,sBAAsB70B,EAAS80B,GAC7B,MAAMI,EAAcl1B,EAAQwB,MAAMyZ,iBAAiB6Z,GAE/CI,GACF3R,GAAYC,iBAAiBxjB,EAAS80B,EAAeI,EAEzD,CAEAP,wBAAwB5a,EAAU+a,GAahC9U,KAAKgV,2BAA2Bjb,GAZH/Z,IAC3B,MAAM5B,EAAQmlB,GAAYQ,iBAAiB/jB,EAAS80B,GAEtC,OAAV12B,GAKJmlB,GAAYE,oBAAoBzjB,EAAS80B,GACzC90B,EAAQwB,MAAMyzB,YAAYH,EAAe12B,IALvC4B,EAAQwB,MAAM2zB,eAAeL,EAKgB,GAInD,CAEAE,2BAA2Bjb,EAAUqb,GACnC,GAAI,GAAUrb,GACZqb,EAASrb,QAIX,IAAK,MAAMsb,KAAOpO,GAAerU,KAAKmH,EAAUiG,KAAKoF,UACnDgQ,EAASC,EAEb,EAcF,MAAMC,GAAS,WAETC,GAAoB,OACpBC,GAAkB,gBAAgBF,KAClCG,GAAY,CAChBC,UAAW,iBACXC,cAAe,KACf/P,YAAY,EACZ9K,WAAW,EAEX8a,YAAa,QAGTC,GAAgB,CACpBH,UAAW,SACXC,cAAe,kBACf/P,WAAY,UACZ9K,UAAW,UACX8a,YAAa,oBAMf,MAAME,WAAiB9R,GACrBU,YAAYL,GACVc,QACAnF,KAAKqF,QAAUrF,KAAKoE,WAAWC,GAC/BrE,KAAK+V,aAAc,EACnB/V,KAAKoF,SAAW,IAClB,CAGWnB,qBACT,OAAOwR,EACT,CAEWvR,yBACT,OAAO2R,EACT,CAEWpZ,kBACT,OAAO6Y,EACT,CAGAzF,KAAKtT,GACH,IAAKyD,KAAKqF,QAAQvK,UAEhB,YADAiC,GAAQR,GAIVyD,KAAKgW,UAEL,MAAMh2B,EAAUggB,KAAKiW,cAEjBjW,KAAKqF,QAAQO,YACf7J,GAAO/b,GAGTA,EAAQwb,UAAUtE,IAAIqe,IAEtBvV,KAAKkW,mBAAkB,KACrBnZ,GAAQR,EAAS,GAErB,CAEAqT,KAAKrT,GACEyD,KAAKqF,QAAQvK,WAKlBkF,KAAKiW,cAAcza,UAAUuH,OAAOwS,IAEpCvV,KAAKkW,mBAAkB,KACrBlW,KAAKuF,UACLxI,GAAQR,EAAS,KARjBQ,GAAQR,EAUZ,CAEAgJ,UACOvF,KAAK+V,cAIVxV,GAAaC,IAAIR,KAAKoF,SAAUoQ,IAEhCxV,KAAKoF,SAASrC,SAEd/C,KAAK+V,aAAc,EACrB,CAGAE,cACE,IAAKjW,KAAKoF,SAAU,CAClB,MAAM+Q,EAAWrwB,SAASswB,cAAc,OACxCD,EAAST,UAAY1V,KAAKqF,QAAQqQ,UAE9B1V,KAAKqF,QAAQO,YACfuQ,EAAS3a,UAAUtE,IAnGD,QAsGpB8I,KAAKoF,SAAW+Q,CAClB,CAEA,OAAOnW,KAAKoF,QACd,CAEAb,kBAAkBF,GAGhB,OADAA,EAAOuR,YAAc/a,GAAWwJ,EAAOuR,aAChCvR,CACT,CAEA2R,UACE,GAAIhW,KAAK+V,YACP,OAGF,MAAM/1B,EAAUggB,KAAKiW,cAErBjW,KAAKqF,QAAQuQ,YAAYS,OAAOr2B,GAEhCugB,GAAaY,GAAGnhB,EAASw1B,IAAiB,KACxCzY,GAAQiD,KAAKqF,QAAQsQ,cAAc,IAErC3V,KAAK+V,aAAc,CACrB,CAEAG,kBAAkB3Z,GAChBS,GAAuBT,EAAUyD,KAAKiW,cAAejW,KAAKqF,QAAQO,WACpE,EAcF,MAEM0Q,GAAc,gBACdC,GAAkB,UAAUD,KAC5BE,GAAoB,cAAcF,KAGlCG,GAAmB,WACnBC,GAAY,CAChBC,WAAW,EACXC,YAAa,MAGTC,GAAgB,CACpBF,UAAW,UACXC,YAAa,WAMf,MAAME,WAAkB9S,GACtBU,YAAYL,GACVc,QACAnF,KAAKqF,QAAUrF,KAAKoE,WAAWC,GAC/BrE,KAAK+W,WAAY,EACjB/W,KAAKgX,qBAAuB,IAC9B,CAGW/S,qBACT,OAAOyS,EACT,CAEWxS,yBACT,OAAO2S,EACT,CAEWpa,kBACT,MAvCW,WAwCb,CAGAwa,WACMjX,KAAK+W,YAIL/W,KAAKqF,QAAQsR,WACf3W,KAAKqF,QAAQuR,YAAYlE,QAG3BnS,GAAaC,IAAI1a,SAAUwwB,IAE3B/V,GAAaY,GAAGrb,SAAUywB,IAAiBnX,GAASY,KAAKkX,eAAe9X,KACxEmB,GAAaY,GAAGrb,SAAU0wB,IAAmBpX,GAASY,KAAKmX,eAAe/X,KAC1EY,KAAK+W,WAAY,EACnB,CAEAK,aACOpX,KAAK+W,YAIV/W,KAAK+W,WAAY,EACjBxW,GAAaC,IAAI1a,SAAUwwB,IAC7B,CAGAY,eAAe9X,GACb,MAAM,YACJwX,GACE5W,KAAKqF,QAET,GAAIjG,EAAMpS,SAAWlH,UAAYsZ,EAAMpS,SAAW4pB,GAAeA,EAAY3xB,SAASma,EAAMpS,QAC1F,OAGF,MAAM1L,EAAW2lB,GAAeU,kBAAkBiP,GAE1B,IAApBt1B,EAAS6P,OACXylB,EAAYlE,QACH1S,KAAKgX,uBAAyBP,GACvCn1B,EAASA,EAAS6P,OAAS,GAAGuhB,QAE9BpxB,EAAS,GAAGoxB,OAEhB,CAEAyE,eAAe/X,GApFD,QAqFRA,EAAM7hB,MAIVyiB,KAAKgX,qBAAuB5X,EAAMiY,SAAWZ,GAxFzB,UAyFtB,EAcF,MAEMa,GAAc,YAGdC,GAAe,OAAOD,KACtBE,GAAyB,gBAAgBF,KACzCG,GAAiB,SAASH,KAC1BI,GAAe,OAAOJ,KACtBK,GAAgB,QAAQL,KACxBM,GAAiB,SAASN,KAC1BO,GAAsB,gBAAgBP,KACtCQ,GAA0B,oBAAoBR,KAC9CS,GAA0B,kBAAkBT,KAC5CU,GAAyB,QAAQV,cACjCW,GAAkB,aAElBC,GAAoB,OACpBC,GAAoB,eAKpBC,GAAY,CAChBjC,UAAU,EACVzD,OAAO,EACP3H,UAAU,GAENsN,GAAgB,CACpBlC,SAAU,mBACVzD,MAAO,UACP3H,SAAU,WAMZ,MAAMuN,WAAcpT,GAClBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAKuY,QAAUtR,GAAeC,QApBV,gBAoBmClH,KAAKoF,UAC5DpF,KAAKwY,UAAYxY,KAAKyY,sBACtBzY,KAAK0Y,WAAa1Y,KAAK2Y,uBACvB3Y,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EACxBnP,KAAK4Y,WAAa,IAAIxE,GAEtBpU,KAAK4L,oBACP,CAGW3H,qBACT,OAAOmU,EACT,CAEWlU,yBACT,OAAOmU,EACT,CAEW5b,kBACT,MA5DW,OA6Db,CAGAsK,OAAOjH,GACL,OAAOE,KAAK2P,SAAW3P,KAAK4P,OAAS5P,KAAK6P,KAAK/P,EACjD,CAEA+P,KAAK/P,GACCE,KAAK2P,UAAY3P,KAAKmP,kBAIR5O,GAAakB,QAAQzB,KAAKoF,SAAUsS,GAAc,CAClE5X,kBAGY+B,mBAId7B,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EAExBnP,KAAK4Y,WAAWhJ,OAEhB9pB,SAAS6G,KAAK6O,UAAUtE,IAAI+gB,IAE5BjY,KAAK6Y,gBAEL7Y,KAAKwY,UAAU3I,MAAK,IAAM7P,KAAK8Y,aAAahZ,KAC9C,CAEA8P,OACO5P,KAAK2P,WAAY3P,KAAKmP,mBAIT5O,GAAakB,QAAQzB,KAAKoF,SAAUmS,IAExC1V,mBAId7B,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EAExBnP,KAAK0Y,WAAWtB,aAEhBpX,KAAKoF,SAAS5J,UAAUuH,OAAOmV,IAE/BlY,KAAK2F,gBAAe,IAAM3F,KAAK+Y,cAAc/Y,KAAKoF,SAAUpF,KAAKgO,gBACnE,CAEAzI,UACE,IAAK,MAAMyT,IAAe,CAAC34B,OAAQ2f,KAAKuY,SACtChY,GAAaC,IAAIwY,EAAa1B,IAGhCtX,KAAKwY,UAAUjT,UAEfvF,KAAK0Y,WAAWtB,aAEhBjS,MAAMI,SACR,CAEA0T,eACEjZ,KAAK6Y,eACP,CAGAJ,sBACE,OAAO,IAAI3C,GAAS,CAClBhb,UAAWgG,QAAQd,KAAKqF,QAAQ8Q,UAEhCvQ,WAAY5F,KAAKgO,eAErB,CAEA2K,uBACE,OAAO,IAAI7B,GAAU,CACnBF,YAAa5W,KAAKoF,UAEtB,CAEA0T,aAAahZ,GAENha,SAAS6G,KAAK1H,SAAS+a,KAAKoF,WAC/Btf,SAAS6G,KAAK0pB,OAAOrW,KAAKoF,UAG5BpF,KAAKoF,SAAS5jB,MAAMwwB,QAAU,QAE9BhS,KAAKoF,SAASxjB,gBAAgB,eAE9Boe,KAAKoF,SAASvjB,aAAa,cAAc,GAEzCme,KAAKoF,SAASvjB,aAAa,OAAQ,UAEnCme,KAAKoF,SAASlZ,UAAY,EAC1B,MAAMgtB,EAAYjS,GAAeC,QA3IT,cA2IsClH,KAAKuY,SAE/DW,IACFA,EAAUhtB,UAAY,GAGxB6P,GAAOiE,KAAKoF,UAEZpF,KAAKoF,SAAS5J,UAAUtE,IAAIghB,IAa5BlY,KAAK2F,gBAXsB,KACrB3F,KAAKqF,QAAQqN,OACf1S,KAAK0Y,WAAWzB,WAGlBjX,KAAKmP,kBAAmB,EACxB5O,GAAakB,QAAQzB,KAAKoF,SAAUuS,GAAe,CACjD7X,iBACA,GAGoCE,KAAKuY,QAASvY,KAAKgO,cAC7D,CAEApC,qBACErL,GAAaY,GAAGnB,KAAKoF,SAAU2S,IAAyB3Y,IACtD,GAtLe,WAsLXA,EAAM7hB,IAIV,OAAIyiB,KAAKqF,QAAQ0F,UACf3L,EAAM+C,sBACNnC,KAAK4P,aAIP5P,KAAKmZ,4BAA4B,IAEnC5Y,GAAaY,GAAG9gB,OAAQu3B,IAAgB,KAClC5X,KAAK2P,WAAa3P,KAAKmP,kBACzBnP,KAAK6Y,eACP,IAEFtY,GAAaY,GAAGnB,KAAKoF,SAAU0S,IAAyB1Y,IAEtDmB,GAAaa,IAAIpB,KAAKoF,SAAUyS,IAAqBuB,IAC/CpZ,KAAKoF,WAAahG,EAAMpS,QAAUgT,KAAKoF,WAAagU,EAAOpsB,SAIjC,WAA1BgT,KAAKqF,QAAQ8Q,SAMbnW,KAAKqF,QAAQ8Q,UACfnW,KAAK4P,OANL5P,KAAKmZ,6BAOP,GACA,GAEN,CAEAJ,aACE/Y,KAAKoF,SAAS5jB,MAAMwwB,QAAU,OAE9BhS,KAAKoF,SAASvjB,aAAa,eAAe,GAE1Cme,KAAKoF,SAASxjB,gBAAgB,cAE9Boe,KAAKoF,SAASxjB,gBAAgB,QAE9Boe,KAAKmP,kBAAmB,EAExBnP,KAAKwY,UAAU5I,MAAK,KAClB9pB,SAAS6G,KAAK6O,UAAUuH,OAAOkV,IAE/BjY,KAAKqZ,oBAELrZ,KAAK4Y,WAAW9lB,QAEhByN,GAAakB,QAAQzB,KAAKoF,SAAUqS,GAAe,GAEvD,CAEAzJ,cACE,OAAOhO,KAAKoF,SAAS5J,UAAUvW,SAtOT,OAuOxB,CAEAk0B,6BAGE,GAFkB5Y,GAAakB,QAAQzB,KAAKoF,SAAUoS,IAExC3V,iBACZ,OAGF,MAAMyX,EAAqBtZ,KAAKoF,SAAStX,aAAehI,SAASC,gBAAgBsC,aAC3EkxB,EAAmBvZ,KAAKoF,SAAS5jB,MAAMiL,UAEpB,WAArB8sB,GAAiCvZ,KAAKoF,SAAS5J,UAAUvW,SAASkzB,MAIjEmB,IACHtZ,KAAKoF,SAAS5jB,MAAMiL,UAAY,UAGlCuT,KAAKoF,SAAS5J,UAAUtE,IAAIihB,IAE5BnY,KAAK2F,gBAAe,KAClB3F,KAAKoF,SAAS5J,UAAUuH,OAAOoV,IAE/BnY,KAAK2F,gBAAe,KAClB3F,KAAKoF,SAAS5jB,MAAMiL,UAAY8sB,CAAgB,GAC/CvZ,KAAKuY,QAAQ,GACfvY,KAAKuY,SAERvY,KAAKoF,SAASsN,QAChB,CAMAmG,gBACE,MAAMS,EAAqBtZ,KAAKoF,SAAStX,aAAehI,SAASC,gBAAgBsC,aAE3E0sB,EAAiB/U,KAAK4Y,WAAWvE,WAEjCmF,EAAoBzE,EAAiB,EAE3C,GAAIyE,IAAsBF,EAAoB,CAC5C,MAAM/2B,EAAW4Z,KAAU,cAAgB,eAC3C6D,KAAKoF,SAAS5jB,MAAMe,GAAY,GAAGwyB,KACrC,CAEA,IAAKyE,GAAqBF,EAAoB,CAC5C,MAAM/2B,EAAW4Z,KAAU,eAAiB,cAC5C6D,KAAKoF,SAAS5jB,MAAMe,GAAY,GAAGwyB,KACrC,CACF,CAEAsE,oBACErZ,KAAKoF,SAAS5jB,MAAMi4B,YAAc,GAClCzZ,KAAKoF,SAAS5jB,MAAMk4B,aAAe,EACrC,CAGA7T,uBAAuBxB,EAAQvE,GAC7B,OAAOE,KAAK4G,MAAK,WACf,MAAM9b,EAAOwtB,GAAMjS,oBAAoBrG,KAAMqE,GAE7C,GAAsB,iBAAXA,EAAX,CAIA,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,GAAQvE,EANb,CAOF,GACF,EAQFS,GAAaY,GAAGrb,SAAUkyB,GApTK,4BAoT2C,SAAU5Y,GAClF,MAAMpS,EAASsN,GAAuB0F,MAElC,CAAC,IAAK,QAAQ9F,SAAS8F,KAAKoG,UAC9BhH,EAAM+C,iBAGR5B,GAAaa,IAAIpU,EAAQ0qB,IAAciC,IACjCA,EAAU9X,kBAKdtB,GAAaa,IAAIpU,EAAQyqB,IAAgB,KACnC3c,GAAUkF,OACZA,KAAK0S,OACP,GACA,IAGJ,MAAMkH,EAAc3S,GAAeC,QA3Ub,eA6UlB0S,GACFtB,GAAMxS,YAAY8T,GAAahK,OAGpB0I,GAAMjS,oBAAoBrZ,GAClC+Z,OAAO/G,KACd,IACAgG,GAAqBsS,IAKrBjc,GAAmBic,IAYnB,MAEMuB,GAAc,gBACdC,GAAiB,YACjBC,GAAwB,OAAOF,KAAcC,KAE7CE,GAAoB,OACpBC,GAAuB,UACvBC,GAAoB,SAEpBC,GAAgB,kBAChBC,GAAe,OAAOP,KACtBQ,GAAgB,QAAQR,KACxBS,GAAe,OAAOT,KACtBU,GAAuB,gBAAgBV,KACvCW,GAAiB,SAASX,KAC1BY,GAAe,SAASZ,KACxBa,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAwB,kBAAkBd,KAE1Ce,GAAY,CAChBzE,UAAU,EACVpL,UAAU,EACV7f,QAAQ,GAEJ2vB,GAAgB,CACpB1E,SAAU,mBACVpL,SAAU,UACV7f,OAAQ,WAMV,MAAM4vB,WAAkB5V,GACtBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAK2P,UAAW,EAChB3P,KAAKwY,UAAYxY,KAAKyY,sBACtBzY,KAAK0Y,WAAa1Y,KAAK2Y,uBAEvB3Y,KAAK4L,oBACP,CAGW3H,qBACT,OAAO2W,EACT,CAEW1W,yBACT,OAAO2W,EACT,CAEWpe,kBACT,MAtDW,WAuDb,CAGAsK,OAAOjH,GACL,OAAOE,KAAK2P,SAAW3P,KAAK4P,OAAS5P,KAAK6P,KAAK/P,EACjD,CAEA+P,KAAK/P,GACCE,KAAK2P,UAISpP,GAAakB,QAAQzB,KAAKoF,SAAUgV,GAAc,CAClEta,kBAGY+B,mBAId7B,KAAK2P,UAAW,EAEhB3P,KAAKwY,UAAU3I,OAEV7P,KAAKqF,QAAQna,SAChB,IAAIkpB,IAAkBxE,OAGxB5P,KAAKoF,SAASvjB,aAAa,cAAc,GAEzCme,KAAKoF,SAASvjB,aAAa,OAAQ,UAEnCme,KAAKoF,SAAS5J,UAAUtE,IAAI+iB,IAgB5Bja,KAAK2F,gBAdoB,KAClB3F,KAAKqF,QAAQna,SAAU8U,KAAKqF,QAAQ8Q,UACvCnW,KAAK0Y,WAAWzB,WAGlBjX,KAAKoF,SAAS5J,UAAUtE,IAAI8iB,IAE5Bha,KAAKoF,SAAS5J,UAAUuH,OAAOkX,IAE/B1Z,GAAakB,QAAQzB,KAAKoF,SAAUiV,GAAe,CACjDva,iBACA,GAGkCE,KAAKoF,UAAU,GACvD,CAEAwK,OACO5P,KAAK2P,WAIQpP,GAAakB,QAAQzB,KAAKoF,SAAUkV,IAExCzY,mBAId7B,KAAK0Y,WAAWtB,aAEhBpX,KAAKoF,SAAS2V,OAEd/a,KAAK2P,UAAW,EAEhB3P,KAAKoF,SAAS5J,UAAUtE,IAAIgjB,IAE5Bla,KAAKwY,UAAU5I,OAgBf5P,KAAK2F,gBAdoB,KACvB3F,KAAKoF,SAAS5J,UAAUuH,OAAOiX,GAAmBE,IAElDla,KAAKoF,SAASxjB,gBAAgB,cAE9Boe,KAAKoF,SAASxjB,gBAAgB,QAEzBoe,KAAKqF,QAAQna,SAChB,IAAIkpB,IAAkBthB,QAGxByN,GAAakB,QAAQzB,KAAKoF,SAAUoV,GAAe,GAGfxa,KAAKoF,UAAU,IACvD,CAEAG,UACEvF,KAAKwY,UAAUjT,UAEfvF,KAAK0Y,WAAWtB,aAEhBjS,MAAMI,SACR,CAGAkT,sBACE,MAUM3d,EAAYgG,QAAQd,KAAKqF,QAAQ8Q,UACvC,OAAO,IAAIL,GAAS,CAClBJ,UA7JsB,qBA8JtB5a,YACA8K,YAAY,EACZgQ,YAAa5V,KAAKoF,SAAS5f,WAC3BmwB,cAAe7a,EAhBK,KACU,WAA1BkF,KAAKqF,QAAQ8Q,SAKjBnW,KAAK4P,OAJHrP,GAAakB,QAAQzB,KAAKoF,SAAUmV,GAI3B,EAUgC,MAE/C,CAEA5B,uBACE,OAAO,IAAI7B,GAAU,CACnBF,YAAa5W,KAAKoF,UAEtB,CAEAwG,qBACErL,GAAaY,GAAGnB,KAAKoF,SAAUuV,IAAuBvb,IAhLvC,WAiLTA,EAAM7hB,MAILyiB,KAAKqF,QAAQ0F,SAKlB/K,KAAK4P,OAJHrP,GAAakB,QAAQzB,KAAKoF,SAAUmV,IAI3B,GAEf,CAGA1U,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOgwB,GAAUzU,oBAAoBrG,KAAMqE,GAEjD,GAAsB,iBAAXA,EAAX,CAIA,QAAqB7K,IAAjB1O,EAAKuZ,IAAyBA,EAAOlK,WAAW,MAAmB,gBAAXkK,EAC1D,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,GAAQrE,KANb,CAOF,GACF,EAQFO,GAAaY,GAAGrb,SAAU40B,GAvMK,gCAuM2C,SAAUtb,GAClF,MAAMpS,EAASsN,GAAuB0F,MAMtC,GAJI,CAAC,IAAK,QAAQ9F,SAAS8F,KAAKoG,UAC9BhH,EAAM+C,iBAGJ9G,GAAW2E,MACb,OAGFO,GAAaa,IAAIpU,EAAQwtB,IAAgB,KAEnC1f,GAAUkF,OACZA,KAAK0S,OACP,IAGF,MAAMkH,EAAc3S,GAAeC,QAAQiT,IAEvCP,GAAeA,IAAgB5sB,GACjC8tB,GAAUhV,YAAY8T,GAAahK,OAGxBkL,GAAUzU,oBAAoBrZ,GACtC+Z,OAAO/G,KACd,IACAO,GAAaY,GAAG9gB,OAAQ05B,IAAuB,KAC7C,IAAK,MAAMhgB,KAAYkN,GAAerU,KAAKunB,IACzCW,GAAUzU,oBAAoBtM,GAAU8V,MAC1C,IAEFtP,GAAaY,GAAG9gB,OAAQo6B,IAAc,KACpC,IAAK,MAAMz6B,KAAWinB,GAAerU,KAAK,gDACG,UAAvClN,iBAAiB1F,GAASiC,UAC5B64B,GAAUzU,oBAAoBrmB,GAAS4vB,MAE3C,IAEF5J,GAAqB8U,IAKrBze,GAAmBye,IAQnB,MAAME,GAAgB,IAAIjkB,IAAI,CAAC,aAAc,OAAQ,OAAQ,WAAY,WAAY,SAAU,MAAO,eAQhGkkB,GAAmB,iEAOnBC,GAAmB,qIAEnBC,GAAmB,CAAC34B,EAAW44B,KACnC,MAAMC,EAAgB74B,EAAUvC,SAASC,cAEzC,OAAIk7B,EAAqBlhB,SAASmhB,IAC5BL,GAAc5jB,IAAIikB,IACbva,QAAQma,GAAiBn3B,KAAKtB,EAAU84B,YAAcJ,GAAiBp3B,KAAKtB,EAAU84B,YAO1FF,EAAqBx0B,QAAO20B,GAAkBA,aAA0BxW,SAAQ7R,MAAKsoB,GAASA,EAAM13B,KAAKu3B,IAAe,EAG3HI,GAAmB,CAEvB,IAAK,CAAC,QAAS,MAAO,KAAM,OAAQ,OAjCP,kBAkC7BnqB,EAAG,CAAC,SAAU,OAAQ,QAAS,OAC/BoqB,KAAM,GACNnqB,EAAG,GACHoqB,GAAI,GACJC,IAAK,GACLC,KAAM,GACNC,IAAK,GACLC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJxqB,EAAG,GACHgb,IAAK,CAAC,MAAO,SAAU,MAAO,QAAS,QAAS,UAChDyP,GAAI,GACJC,GAAI,GACJC,EAAG,GACHC,IAAK,GACLC,EAAG,GACHC,MAAO,GACPC,KAAM,GACNC,IAAK,GACLC,IAAK,GACLC,OAAQ,GACRC,EAAG,GACHC,GAAI,IA+CAC,GAAY,CAChBC,UAAW3B,GACX4B,QAAS,CAAC,EAEVC,WAAY,GACZhwB,MAAM,EACNiwB,UAAU,EACVC,WAAY,KACZC,SAAU,eAENC,GAAgB,CACpBN,UAAW,SACXC,QAAS,SACTC,WAAY,oBACZhwB,KAAM,UACNiwB,SAAU,UACVC,WAAY,kBACZC,SAAU,UAENE,GAAqB,CACzBC,MAAO,iCACP7jB,SAAU,oBAMZ,MAAM8jB,WAAwB7Z,GAC5BU,YAAYL,GACVc,QACAnF,KAAKqF,QAAUrF,KAAKoE,WAAWC,EACjC,CAGWJ,qBACT,OAAOkZ,EACT,CAEWjZ,yBACT,OAAOwZ,EACT,CAEWjhB,kBACT,MA5CW,iBA6Cb,CAGAqhB,aACE,OAAOrgC,OAAO0hB,OAAOa,KAAKqF,QAAQgY,SAAS95B,KAAI8gB,GAAUrE,KAAK+d,yBAAyB1Z,KAASzd,OAAOka,QACzG,CAEAkd,aACE,OAAOhe,KAAK8d,aAAa3sB,OAAS,CACpC,CAEA8sB,cAAcZ,GAMZ,OALArd,KAAKke,cAAcb,GAEnBrd,KAAKqF,QAAQgY,QAAU,IAAKrd,KAAKqF,QAAQgY,WACpCA,GAEErd,IACT,CAEAme,SACE,MAAMC,EAAkBt4B,SAASswB,cAAc,OAC/CgI,EAAgBC,UAAYre,KAAKse,eAAete,KAAKqF,QAAQoY,UAE7D,IAAK,MAAO1jB,EAAUwkB,KAAS9gC,OAAO4kB,QAAQrC,KAAKqF,QAAQgY,SACzDrd,KAAKwe,YAAYJ,EAAiBG,EAAMxkB,GAG1C,MAAM0jB,EAAWW,EAAgBjX,SAAS,GAEpCmW,EAAatd,KAAK+d,yBAAyB/d,KAAKqF,QAAQiY,YAM9D,OAJIA,GACFG,EAASjiB,UAAUtE,OAAOomB,EAAW36B,MAAM,MAGtC86B,CACT,CAGAjZ,iBAAiBH,GACfc,MAAMX,iBAAiBH,GAEvBrE,KAAKke,cAAc7Z,EAAOgZ,QAC5B,CAEAa,cAAcO,GACZ,IAAK,MAAO1kB,EAAUsjB,KAAY5/B,OAAO4kB,QAAQoc,GAC/CtZ,MAAMX,iBAAiB,CACrBzK,WACA6jB,MAAOP,GACNM,GAEP,CAEAa,YAAYf,EAAUJ,EAAStjB,GAC7B,MAAM2kB,EAAkBzX,GAAeC,QAAQnN,EAAU0jB,GAEpDiB,KAILrB,EAAUrd,KAAK+d,yBAAyBV,IAOpC,GAAUA,GACZrd,KAAK2e,sBAAsB9jB,GAAWwiB,GAAUqB,GAK9C1e,KAAKqF,QAAQ/X,KACfoxB,EAAgBL,UAAYre,KAAKse,eAAejB,GAIlDqB,EAAgBE,YAAcvB,EAf5BqB,EAAgB3b,SAgBpB,CAEAub,eAAeG,GACb,OAAOze,KAAKqF,QAAQkY,SA7KxB,SAAsBsB,EAAYzB,EAAW0B,GAC3C,IAAKD,EAAW1tB,OACd,OAAO0tB,EAGT,GAAIC,GAAgD,mBAArBA,EAC7B,OAAOA,EAAiBD,GAG1B,MACME,GADY,IAAI1+B,OAAO2+B,WACKC,gBAAgBJ,EAAY,aACxDv9B,EAAW,GAAGlC,UAAU2/B,EAAgBpyB,KAAKyT,iBAAiB,MAEpE,IAAK,MAAMpgB,KAAWsB,EAAU,CAC9B,MAAM49B,EAAcl/B,EAAQC,SAASC,cAErC,IAAKzC,OAAO4D,KAAK+7B,GAAWljB,SAASglB,GAAc,CACjDl/B,EAAQ+iB,SACR,QACF,CAEA,MAAMoc,EAAgB,GAAG//B,UAAUY,EAAQ0B,YACrC09B,EAAoB,GAAGhgC,OAAOg+B,EAAU,MAAQ,GAAIA,EAAU8B,IAAgB,IAEpF,IAAK,MAAM18B,KAAa28B,EACjBhE,GAAiB34B,EAAW48B,IAC/Bp/B,EAAQ4B,gBAAgBY,EAAUvC,SAGxC,CAEA,OAAO8+B,EAAgBpyB,KAAK0xB,SAC9B,CA6ImCgB,CAAaZ,EAAKze,KAAKqF,QAAQ+X,UAAWpd,KAAKqF,QAAQmY,YAAciB,CACtG,CAEAV,yBAAyBU,GACvB,MAAsB,mBAARA,EAAqBA,EAAIze,MAAQye,CACjD,CAEAE,sBAAsB3+B,EAAS0+B,GAC7B,GAAI1e,KAAKqF,QAAQ/X,KAGf,OAFAoxB,EAAgBL,UAAY,QAC5BK,EAAgBrI,OAAOr2B,GAIzB0+B,EAAgBE,YAAc5+B,EAAQ4+B,WACxC,EAcF,MACMU,GAAwB,IAAIvoB,IAAI,CAAC,WAAY,YAAa,eAC1DwoB,GAAoB,OAEpBC,GAAoB,OAEpBC,GAAiB,SACjBC,GAAmB,gBACnBC,GAAgB,QAChBC,GAAgB,QAahBC,GAAgB,CACpBC,KAAM,OACNC,IAAK,MACLC,MAAO7jB,KAAU,OAAS,QAC1B8jB,OAAQ,SACRC,KAAM/jB,KAAU,QAAU,QAEtBgkB,GAAY,CAChB/C,UAAW3B,GACX2E,WAAW,EACX1xB,SAAU,kBACV2xB,WAAW,EACXC,YAAa,GACbC,MAAO,EACP9vB,mBAAoB,CAAC,MAAO,QAAS,SAAU,QAC/CnD,MAAM,EACN7E,OAAQ,CAAC,EAAG,GACZtJ,UAAW,MACX8yB,aAAc,KACdsL,UAAU,EACVC,WAAY,KACZzjB,UAAU,EACV0jB,SAAU,+GACV+C,MAAO,GACP/e,QAAS,eAELgf,GAAgB,CACpBrD,UAAW,SACXgD,UAAW,UACX1xB,SAAU,mBACV2xB,UAAW,2BACXC,YAAa,oBACbC,MAAO,kBACP9vB,mBAAoB,QACpBnD,KAAM,UACN7E,OAAQ,0BACRtJ,UAAW,oBACX8yB,aAAc,yBACdsL,SAAU,UACVC,WAAY,kBACZzjB,SAAU,mBACV0jB,SAAU,SACV+C,MAAO,4BACP/e,QAAS,UAMX,MAAMif,WAAgBxb,GACpBR,YAAY1kB,EAASqkB,GACnB,QAAsB,IAAX,EACT,MAAM,IAAIW,UAAU,+DAGtBG,MAAMnlB,EAASqkB,GAEfrE,KAAK2gB,YAAa,EAClB3gB,KAAK4gB,SAAW,EAChB5gB,KAAK6gB,WAAa,KAClB7gB,KAAK8gB,eAAiB,CAAC,EACvB9gB,KAAKoS,QAAU,KACfpS,KAAK+gB,iBAAmB,KACxB/gB,KAAKghB,YAAc,KAEnBhhB,KAAKihB,IAAM,KAEXjhB,KAAKkhB,gBAEAlhB,KAAKqF,QAAQtL,UAChBiG,KAAKmhB,WAET,CAGWld,qBACT,OAAOkc,EACT,CAEWjc,yBACT,OAAOuc,EACT,CAEWhkB,kBACT,MA1GW,SA2Gb,CAGA2kB,SACEphB,KAAK2gB,YAAa,CACpB,CAEAU,UACErhB,KAAK2gB,YAAa,CACpB,CAEAW,gBACEthB,KAAK2gB,YAAc3gB,KAAK2gB,UAC1B,CAEA5Z,SACO/G,KAAK2gB,aAIV3gB,KAAK8gB,eAAeS,OAASvhB,KAAK8gB,eAAeS,MAE7CvhB,KAAK2P,WACP3P,KAAKwhB,SAKPxhB,KAAKyhB,SACP,CAEAlc,UACE0H,aAAajN,KAAK4gB,UAClBrgB,GAAaC,IAAIR,KAAKoF,SAASjK,QAAQskB,IAAiBC,GAAkB1f,KAAK0hB,mBAE3E1hB,KAAKoF,SAASpL,aAAa,2BAC7BgG,KAAKoF,SAASvjB,aAAa,QAASme,KAAKoF,SAASpL,aAAa,2BAGjEgG,KAAK2hB,iBAELxc,MAAMI,SACR,CAEAsK,OACE,GAAoC,SAAhC7P,KAAKoF,SAAS5jB,MAAMwwB,QACtB,MAAM,IAAI7N,MAAM,uCAGlB,IAAMnE,KAAK4hB,mBAAoB5hB,KAAK2gB,WAClC,OAGF,MAAMhH,EAAYpZ,GAAakB,QAAQzB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UAlJtD,SAqJXkU,GAFalmB,GAAeqE,KAAKoF,WAELpF,KAAKoF,SAAS7kB,cAAcwF,iBAAiBd,SAAS+a,KAAKoF,UAE7F,GAAIuU,EAAU9X,mBAAqBggB,EACjC,OAIF7hB,KAAK2hB,iBAEL,MAAMV,EAAMjhB,KAAK8hB,iBAEjB9hB,KAAKoF,SAASvjB,aAAa,mBAAoBo/B,EAAIjnB,aAAa,OAEhE,MAAM,UACJqmB,GACErgB,KAAKqF,QAaT,GAXKrF,KAAKoF,SAAS7kB,cAAcwF,gBAAgBd,SAAS+a,KAAKihB,OAC7DZ,EAAUhK,OAAO4K,GACjB1gB,GAAakB,QAAQzB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UAtKpC,cAyKnB3N,KAAKoS,QAAUpS,KAAKyS,cAAcwO,GAClCA,EAAIzlB,UAAUtE,IAAIsoB,IAKd,iBAAkB15B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAKwa,UAC/C5G,GAAaY,GAAGnhB,EAAS,YAAa8b,IAc1CkE,KAAK2F,gBAVY,KACfpF,GAAakB,QAAQzB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UAvLrC,WAyLQ,IAApB3N,KAAK6gB,YACP7gB,KAAKwhB,SAGPxhB,KAAK6gB,YAAa,CAAK,GAGK7gB,KAAKihB,IAAKjhB,KAAKgO,cAC/C,CAEA4B,OACE,GAAK5P,KAAK2P,aAIQpP,GAAakB,QAAQzB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UA3MtD,SA6MH9L,iBAAd,CASA,GALY7B,KAAK8hB,iBAEbtmB,UAAUuH,OAAOyc,IAGjB,iBAAkB15B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAKwa,UAC/C5G,GAAaC,IAAIxgB,EAAS,YAAa8b,IAI3CkE,KAAK8gB,eAA4B,OAAI,EACrC9gB,KAAK8gB,eAAelB,KAAiB,EACrC5f,KAAK8gB,eAAenB,KAAiB,EACrC3f,KAAK6gB,WAAa,KAgBlB7gB,KAAK2F,gBAdY,KACX3F,KAAK+hB,yBAIJ/hB,KAAK6gB,YACR7gB,KAAK2hB,iBAGP3hB,KAAKoF,SAASxjB,gBAAgB,oBAE9B2e,GAAakB,QAAQzB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UA3OpC,WA2O8D,GAGnD3N,KAAKihB,IAAKjhB,KAAKgO,cAhC7C,CAiCF,CAEAxiB,SACMwU,KAAKoS,SACPpS,KAAKoS,QAAQ5mB,QAEjB,CAGAo2B,iBACE,OAAO9gB,QAAQd,KAAKgiB,YACtB,CAEAF,iBAKE,OAJK9hB,KAAKihB,MACRjhB,KAAKihB,IAAMjhB,KAAKiiB,kBAAkBjiB,KAAKghB,aAAehhB,KAAKkiB,2BAGtDliB,KAAKihB,GACd,CAEAgB,kBAAkB5E,GAChB,MAAM4D,EAAMjhB,KAAKmiB,oBAAoB9E,GAASc,SAG9C,IAAK8C,EACH,OAAO,KAGTA,EAAIzlB,UAAUuH,OAAOwc,GAAmBC,IAExCyB,EAAIzlB,UAAUtE,IAAI,MAAM8I,KAAK0E,YAAYjI,aACzC,MAAM2lB,EA92HKC,KACb,GACEA,GAAUz/B,KAAK0/B,MAlBH,IAkBS1/B,KAAK2/B,gBACnBz8B,SAAS08B,eAAeH,IAEjC,OAAOA,CAAM,EAy2HGI,CAAOziB,KAAK0E,YAAYjI,MAAMnc,WAO5C,OANA2gC,EAAIp/B,aAAa,KAAMugC,GAEnBpiB,KAAKgO,eACPiT,EAAIzlB,UAAUtE,IAAIqoB,IAGb0B,CACT,CAEAyB,WAAWrF,GACTrd,KAAKghB,YAAc3D,EAEfrd,KAAK2P,aACP3P,KAAK2hB,iBAEL3hB,KAAK6P,OAET,CAEAsS,oBAAoB9E,GAYlB,OAXIrd,KAAK+gB,iBACP/gB,KAAK+gB,iBAAiB9C,cAAcZ,GAEpCrd,KAAK+gB,iBAAmB,IAAIlD,GAAgB,IAAK7d,KAAKqF,QAGpDgY,UACAC,WAAYtd,KAAK+d,yBAAyB/d,KAAKqF,QAAQib,eAIpDtgB,KAAK+gB,gBACd,CAEAmB,yBACE,MAAO,CACL,iBAA0BliB,KAAKgiB,YAEnC,CAEAA,YACE,OAAOhiB,KAAK+d,yBAAyB/d,KAAKqF,QAAQmb,QAAUxgB,KAAKoF,SAASpL,aAAa,yBACzF,CAGA2oB,6BAA6BvjB,GAC3B,OAAOY,KAAK0E,YAAY2B,oBAAoBjH,EAAMW,eAAgBC,KAAK4iB,qBACzE,CAEA5U,cACE,OAAOhO,KAAKqF,QAAQ+a,WAAapgB,KAAKihB,KAAOjhB,KAAKihB,IAAIzlB,UAAUvW,SAASs6B,GAC3E,CAEA5P,WACE,OAAO3P,KAAKihB,KAAOjhB,KAAKihB,IAAIzlB,UAAUvW,SAASu6B,GACjD,CAEA/M,cAAcwO,GACZ,MAAM9hC,EAA8C,mBAA3B6gB,KAAKqF,QAAQlmB,UAA2B6gB,KAAKqF,QAAQlmB,UAAUlB,KAAK+hB,KAAMihB,EAAKjhB,KAAKoF,UAAYpF,KAAKqF,QAAQlmB,UAChI0jC,EAAahD,GAAc1gC,EAAU8lB,eAC3C,OAAO,GAAoBjF,KAAKoF,SAAU6b,EAAKjhB,KAAK6S,iBAAiBgQ,GACvE,CAEA5P,aACE,MAAM,OACJxqB,GACEuX,KAAKqF,QAET,MAAsB,iBAAX5c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAASmf,OAAO+P,SAASlvB,EAAO,MAGzC,mBAAXqK,EACFyqB,GAAczqB,EAAOyqB,EAAYlT,KAAKoF,UAGxC3c,CACT,CAEAs1B,yBAAyBU,GACvB,MAAsB,mBAARA,EAAqBA,EAAIxgC,KAAK+hB,KAAKoF,UAAYqZ,CAC/D,CAEA5L,iBAAiBgQ,GACf,MAAM1P,EAAwB,CAC5Bh0B,UAAW0jC,EACXhsB,UAAW,CAAC,CACV9V,KAAM,OACNmB,QAAS,CACPuO,mBAAoBuP,KAAKqF,QAAQ5U,qBAElC,CACD1P,KAAM,SACNmB,QAAS,CACPuG,OAAQuX,KAAKiT,eAEd,CACDlyB,KAAM,kBACNmB,QAAS,CACPwM,SAAUsR,KAAKqF,QAAQ3W,WAExB,CACD3N,KAAM,QACNmB,QAAS,CACPlC,QAAS,IAAIggB,KAAK0E,YAAYjI,eAE/B,CACD1b,KAAM,kBACNC,SAAS,EACTC,MAAO,aACPC,GAAI4J,IAGFkV,KAAK8hB,iBAAiBjgC,aAAa,wBAAyBiJ,EAAK1J,MAAMjC,UAAU,KAIvF,MAAO,IAAKg0B,KAC+B,mBAA9BnT,KAAKqF,QAAQ4M,aAA8BjS,KAAKqF,QAAQ4M,aAAakB,GAAyBnT,KAAKqF,QAAQ4M,aAE1H,CAEAiP,gBACE,MAAM4B,EAAW9iB,KAAKqF,QAAQ5D,QAAQ9e,MAAM,KAE5C,IAAK,MAAM8e,KAAWqhB,EACpB,GAAgB,UAAZrhB,EACFlB,GAAaY,GAAGnB,KAAKoF,SAAUpF,KAAK0E,YAAYiJ,UA3YlC,SA2Y4D3N,KAAKqF,QAAQtL,UAAUqF,IAC/EY,KAAK2iB,6BAA6BvjB,GAE1C2H,QAAQ,SAEb,GAtZU,WAsZNtF,EAA4B,CACrC,MAAMshB,EAAUthB,IAAYke,GAAgB3f,KAAK0E,YAAYiJ,UA9Y5C,cA8Y0E3N,KAAK0E,YAAYiJ,UAhZ5F,WAiZVqV,EAAWvhB,IAAYke,GAAgB3f,KAAK0E,YAAYiJ,UA9Y7C,cA8Y2E3N,KAAK0E,YAAYiJ,UAhZ5F,YAiZjBpN,GAAaY,GAAGnB,KAAKoF,SAAU2d,EAAS/iB,KAAKqF,QAAQtL,UAAUqF,IAC7D,MAAMkU,EAAUtT,KAAK2iB,6BAA6BvjB,GAElDkU,EAAQwN,eAA8B,YAAf1hB,EAAMqB,KAAqBmf,GAAgBD,KAAiB,EAEnFrM,EAAQmO,QAAQ,IAElBlhB,GAAaY,GAAGnB,KAAKoF,SAAU4d,EAAUhjB,KAAKqF,QAAQtL,UAAUqF,IAC9D,MAAMkU,EAAUtT,KAAK2iB,6BAA6BvjB,GAElDkU,EAAQwN,eAA8B,aAAf1hB,EAAMqB,KAAsBmf,GAAgBD,IAAiBrM,EAAQlO,SAASngB,SAASma,EAAMU,eAEpHwT,EAAQkO,QAAQ,GAEpB,CAGFxhB,KAAK0hB,kBAAoB,KACnB1hB,KAAKoF,UACPpF,KAAK4P,MACP,EAGFrP,GAAaY,GAAGnB,KAAKoF,SAASjK,QAAQskB,IAAiBC,GAAkB1f,KAAK0hB,kBAChF,CAEAP,YACE,MAAMX,EAAQxgB,KAAKoF,SAASpL,aAAa,SAEpCwmB,IAIAxgB,KAAKoF,SAASpL,aAAa,eAAkBgG,KAAKoF,SAASwZ,YAAYxkB,QAC1E4F,KAAKoF,SAASvjB,aAAa,aAAc2+B,GAG3CxgB,KAAKoF,SAASvjB,aAAa,yBAA0B2+B,GAGrDxgB,KAAKoF,SAASxjB,gBAAgB,SAChC,CAEA6/B,SACMzhB,KAAK2P,YAAc3P,KAAK6gB,WAC1B7gB,KAAK6gB,YAAa,GAIpB7gB,KAAK6gB,YAAa,EAElB7gB,KAAKijB,aAAY,KACXjjB,KAAK6gB,YACP7gB,KAAK6P,MACP,GACC7P,KAAKqF,QAAQkb,MAAM1Q,MACxB,CAEA2R,SACMxhB,KAAK+hB,yBAIT/hB,KAAK6gB,YAAa,EAElB7gB,KAAKijB,aAAY,KACVjjB,KAAK6gB,YACR7gB,KAAK4P,MACP,GACC5P,KAAKqF,QAAQkb,MAAM3Q,MACxB,CAEAqT,YAAYrlB,EAASslB,GACnBjW,aAAajN,KAAK4gB,UAClB5gB,KAAK4gB,SAAW/iB,WAAWD,EAASslB,EACtC,CAEAnB,uBACE,OAAOtkC,OAAO0hB,OAAOa,KAAK8gB,gBAAgB5mB,UAAS,EACrD,CAEAkK,WAAWC,GACT,MAAM8e,EAAiB5f,GAAYG,kBAAkB1D,KAAKoF,UAE1D,IAAK,MAAMge,KAAiB3lC,OAAO4D,KAAK8hC,GAClC7D,GAAsBloB,IAAIgsB,WACrBD,EAAeC,GAY1B,OARA/e,EAAS,IAAK8e,KACU,iBAAX9e,GAAuBA,EAASA,EAAS,CAAC,GAEvDA,EAASrE,KAAKsE,gBAAgBD,GAC9BA,EAASrE,KAAKuE,kBAAkBF,GAEhCrE,KAAKwE,iBAAiBH,GAEfA,CACT,CAEAE,kBAAkBF,GAkBhB,OAjBAA,EAAOgc,WAAiC,IAArBhc,EAAOgc,UAAsBv6B,SAAS6G,KAAOkO,GAAWwJ,EAAOgc,WAEtD,iBAAjBhc,EAAOkc,QAChBlc,EAAOkc,MAAQ,CACb1Q,KAAMxL,EAAOkc,MACb3Q,KAAMvL,EAAOkc,QAIW,iBAAjBlc,EAAOmc,QAChBnc,EAAOmc,MAAQnc,EAAOmc,MAAMlgC,YAGA,iBAAnB+jB,EAAOgZ,UAChBhZ,EAAOgZ,QAAUhZ,EAAOgZ,QAAQ/8B,YAG3B+jB,CACT,CAEAue,qBACE,MAAMve,EAAS,CAAC,EAEhB,IAAK,MAAM9mB,KAAOyiB,KAAKqF,QACjBrF,KAAK0E,YAAYT,QAAQ1mB,KAASyiB,KAAKqF,QAAQ9nB,KACjD8mB,EAAO9mB,GAAOyiB,KAAKqF,QAAQ9nB,IAS/B,OALA8mB,EAAOtK,UAAW,EAClBsK,EAAO5C,QAAU,SAIV4C,CACT,CAEAsd,iBACM3hB,KAAKoS,UACPpS,KAAKoS,QAAQ3Y,UAEbuG,KAAKoS,QAAU,MAGbpS,KAAKihB,MACPjhB,KAAKihB,IAAIle,SACT/C,KAAKihB,IAAM,KAEf,CAGApb,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAO41B,GAAQra,oBAAoBrG,KAAMqE,GAE/C,GAAsB,iBAAXA,EAAX,CAIA,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IANL,CAOF,GACF,EAQFhI,GAAmBqkB,IAYnB,MAGM2C,GAAY,IAAK3C,GAAQzc,QAC7BoZ,QAAS,GACT50B,OAAQ,CAAC,EAAG,GACZtJ,UAAW,QACXs+B,SAAU,8IACVhc,QAAS,SAEL6hB,GAAgB,IAAK5C,GAAQxc,YACjCmZ,QAAS,kCAMX,MAAMkG,WAAgB7C,GAETzc,qBACT,OAAOof,EACT,CAEWnf,yBACT,OAAOof,EACT,CAEW7mB,kBACT,MA5BW,SA6Bb,CAGAmlB,iBACE,OAAO5hB,KAAKgiB,aAAehiB,KAAKwjB,aAClC,CAGAtB,yBACE,MAAO,CACL,kBAAkBliB,KAAKgiB,YACvB,gBAAoBhiB,KAAKwjB,cAE7B,CAEAA,cACE,OAAOxjB,KAAK+d,yBAAyB/d,KAAKqF,QAAQgY,QACpD,CAGAxX,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOy4B,GAAQld,oBAAoBrG,KAAMqE,GAE/C,GAAsB,iBAAXA,EAAX,CAIA,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IANL,CAOF,GACF,EAQFhI,GAAmBknB,IAYnB,MAEME,GAAc,gBAEdC,GAAiB,WAAWD,KAC5BE,GAAc,QAAQF,KACtBG,GAAwB,OAAOH,cAE/BI,GAAsB,SAEtBC,GAAwB,SAExBC,GAAqB,YAGrBC,GAAsB,GAAGD,mBAA+CA,uBAGxEE,GAAY,CAChBx7B,OAAQ,KAERy7B,WAAY,eACZC,cAAc,EACdn3B,OAAQ,KACRo3B,UAAW,CAAC,GAAK,GAAK,IAElBC,GAAgB,CACpB57B,OAAQ,gBAERy7B,WAAY,SACZC,aAAc,UACdn3B,OAAQ,UACRo3B,UAAW,SAMb,MAAME,WAAkBpf,GACtBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GAEfrE,KAAKukB,aAAe,IAAI5yB,IACxBqO,KAAKwkB,oBAAsB,IAAI7yB,IAC/BqO,KAAKykB,aAA6D,YAA9C/+B,iBAAiBsa,KAAKoF,UAAU3Y,UAA0B,KAAOuT,KAAKoF,SAC1FpF,KAAK0kB,cAAgB,KACrB1kB,KAAK2kB,UAAY,KACjB3kB,KAAK4kB,oBAAsB,CACzBC,gBAAiB,EACjBC,gBAAiB,GAEnB9kB,KAAK+kB,SACP,CAGW9gB,qBACT,OAAOggB,EACT,CAEW/f,yBACT,OAAOmgB,EACT,CAEW5nB,kBACT,MAhEW,WAiEb,CAGAsoB,UACE/kB,KAAKglB,mCAELhlB,KAAKilB,2BAEDjlB,KAAK2kB,UACP3kB,KAAK2kB,UAAUO,aAEfllB,KAAK2kB,UAAY3kB,KAAKmlB,kBAGxB,IAAK,MAAMC,KAAWplB,KAAKwkB,oBAAoBrlB,SAC7Ca,KAAK2kB,UAAUU,QAAQD,EAE3B,CAEA7f,UACEvF,KAAK2kB,UAAUO,aAEf/f,MAAMI,SACR,CAGAhB,kBAAkBF,GAUhB,OARAA,EAAOrX,OAAS6N,GAAWwJ,EAAOrX,SAAWlH,SAAS6G,KAEtD0X,EAAO6f,WAAa7f,EAAO5b,OAAS,GAAG4b,EAAO5b,oBAAsB4b,EAAO6f,WAE3C,iBAArB7f,EAAO+f,YAChB/f,EAAO+f,UAAY/f,EAAO+f,UAAUzhC,MAAM,KAAKY,KAAInF,GAASmf,OAAOC,WAAWpf,MAGzEimB,CACT,CAEA4gB,2BACOjlB,KAAKqF,QAAQ8e,eAKlB5jB,GAAaC,IAAIR,KAAKqF,QAAQrY,OAAQ22B,IACtCpjB,GAAaY,GAAGnB,KAAKqF,QAAQrY,OAAQ22B,GAAaG,IAAuB1kB,IACvE,MAAMkmB,EAAoBtlB,KAAKwkB,oBAAoB5mC,IAAIwhB,EAAMpS,OAAOtB,MAEpE,GAAI45B,EAAmB,CACrBlmB,EAAM+C,iBACN,MAAMtG,EAAOmE,KAAKykB,cAAgBpkC,OAC5BmE,EAAS8gC,EAAkBxgC,UAAYkb,KAAKoF,SAAStgB,UAE3D,GAAI+W,EAAK0pB,SAKP,YAJA1pB,EAAK0pB,SAAS,CACZnjC,IAAKoC,EACLghC,SAAU,WAMd3pB,EAAK3P,UAAY1H,CACnB,KAEJ,CAEA2gC,kBACE,MAAMjjC,EAAU,CACd2Z,KAAMmE,KAAKykB,aACXL,UAAWpkB,KAAKqF,QAAQ+e,UACxBF,WAAYlkB,KAAKqF,QAAQ6e,YAE3B,OAAO,IAAIuB,sBAAqBpjB,GAAWrC,KAAK0lB,kBAAkBrjB,IAAUngB,EAC9E,CAGAwjC,kBAAkBrjB,GAChB,MAAMsjB,EAAgB/H,GAAS5d,KAAKukB,aAAa3mC,IAAI,IAAIggC,EAAM5wB,OAAO44B,MAEhE3O,EAAW2G,IACf5d,KAAK4kB,oBAAoBC,gBAAkBjH,EAAM5wB,OAAOlI,UAExDkb,KAAK6lB,SAASF,EAAc/H,GAAO,EAG/BkH,GAAmB9kB,KAAKykB,cAAgB3+B,SAASC,iBAAiBmG,UAClE45B,EAAkBhB,GAAmB9kB,KAAK4kB,oBAAoBE,gBACpE9kB,KAAK4kB,oBAAoBE,gBAAkBA,EAE3C,IAAK,MAAMlH,KAASvb,EAAS,CAC3B,IAAKub,EAAMmI,eAAgB,CACzB/lB,KAAK0kB,cAAgB,KAErB1kB,KAAKgmB,kBAAkBL,EAAc/H,IAErC,QACF,CAEA,MAAMqI,EAA2BrI,EAAM5wB,OAAOlI,WAAakb,KAAK4kB,oBAAoBC,gBAEpF,GAAIiB,GAAmBG,GAGrB,GAFAhP,EAAS2G,IAEJkH,EACH,YAOCgB,GAAoBG,GACvBhP,EAAS2G,EAEb,CACF,CAEAoH,mCACEhlB,KAAKukB,aAAe,IAAI5yB,IACxBqO,KAAKwkB,oBAAsB,IAAI7yB,IAC/B,MAAMu0B,EAAcjf,GAAerU,KAAKkxB,GAAuB9jB,KAAKqF,QAAQrY,QAE5E,IAAK,MAAMm5B,KAAUD,EAAa,CAEhC,IAAKC,EAAOz6B,MAAQ2P,GAAW8qB,GAC7B,SAGF,MAAMb,EAAoBre,GAAeC,QAAQif,EAAOz6B,KAAMsU,KAAKoF,UAE/DtK,GAAUwqB,KACZtlB,KAAKukB,aAAa/xB,IAAI2zB,EAAOz6B,KAAMy6B,GAEnCnmB,KAAKwkB,oBAAoBhyB,IAAI2zB,EAAOz6B,KAAM45B,GAE9C,CACF,CAEAO,SAAS74B,GACHgT,KAAK0kB,gBAAkB13B,IAI3BgT,KAAKgmB,kBAAkBhmB,KAAKqF,QAAQrY,QAEpCgT,KAAK0kB,cAAgB13B,EACrBA,EAAOwO,UAAUtE,IAAI2sB,IAErB7jB,KAAKomB,iBAAiBp5B,GAEtBuT,GAAakB,QAAQzB,KAAKoF,SAAUse,GAAgB,CAClD5jB,cAAe9S,IAEnB,CAEAo5B,iBAAiBp5B,GAEf,GAAIA,EAAOwO,UAAUvW,SAzNQ,iBA0N3BgiB,GAAeC,QAhNc,mBAgNsBla,EAAOmO,QAjNtC,cAiNkEK,UAAUtE,IAAI2sB,SAItG,IAAK,MAAMwC,KAAapf,GAAeI,QAAQra,EA1NnB,qBA6N1B,IAAK,MAAMxJ,KAAQyjB,GAAeM,KAAK8e,EAAWrC,IAChDxgC,EAAKgY,UAAUtE,IAAI2sB,GAGzB,CAEAmC,kBAAkB9gC,GAChBA,EAAOsW,UAAUuH,OAAO8gB,IACxB,MAAMyC,EAAcrf,GAAerU,KAAK,GAAGkxB,MAAyBD,KAAuB3+B,GAE3F,IAAK,MAAM9E,KAAQkmC,EACjBlmC,EAAKob,UAAUuH,OAAO8gB,GAE1B,CAGAhe,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAOw5B,GAAUje,oBAAoBrG,KAAMqE,GAEjD,GAAsB,iBAAXA,EAAX,CAIA,QAAqB7K,IAAjB1O,EAAKuZ,IAAyBA,EAAOlK,WAAW,MAAmB,gBAAXkK,EAC1D,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IANL,CAOF,GACF,EAQF9D,GAAaY,GAAG9gB,OAAQujC,IAAuB,KAC7C,IAAK,MAAM2C,KAAOtf,GAAerU,KAtQT,0BAuQtB0xB,GAAUje,oBAAoBkgB,EAChC,IAMFlqB,GAAmBioB,IAYnB,MAEMkC,GAAc,UACdC,GAAe,OAAOD,KACtBE,GAAiB,SAASF,KAC1BG,GAAe,OAAOH,KACtBI,GAAgB,QAAQJ,KACxBK,GAAuB,QAAQL,KAC/BM,GAAgB,UAAUN,KAC1BO,GAAsB,OAAOP,KAC7BQ,GAAiB,YACjBC,GAAkB,aAClBC,GAAe,UACfC,GAAiB,YACjBC,GAAoB,SACpBC,GAAoB,OACpBC,GAAoB,OAIpBC,GAA+B,yBAI/BC,GAAuB,2EAEvBC,GAAsB,YAHOF,uBAAiDA,mBAA6CA,OAG/EC,KAC5CE,GAA8B,IAAIN,8BAA6CA,+BAA8CA,4BAKnI,MAAMO,WAAYziB,GAChBR,YAAY1kB,GACVmlB,MAAMnlB,GACNggB,KAAKqS,QAAUrS,KAAKoF,SAASjK,QAdN,uCAgBlB6E,KAAKqS,UAMVrS,KAAK4nB,sBAAsB5nB,KAAKqS,QAASrS,KAAK6nB,gBAE9CtnB,GAAaY,GAAGnB,KAAKoF,SAAU0hB,IAAe1nB,GAASY,KAAK4M,SAASxN,KACvE,CAGW3C,kBACT,MAlDW,KAmDb,CAGAoT,OAEE,MAAMiY,EAAY9nB,KAAKoF,SAEvB,GAAIpF,KAAK+nB,cAAcD,GACrB,OAIF,MAAME,EAAShoB,KAAKioB,iBAEdC,EAAYF,EAASznB,GAAakB,QAAQumB,EAAQvB,GAAc,CACpE3mB,cAAegoB,IACZ,KACavnB,GAAakB,QAAQqmB,EAAWnB,GAAc,CAC9D7mB,cAAekoB,IAGHnmB,kBAAoBqmB,GAAaA,EAAUrmB,mBAIzD7B,KAAKmoB,YAAYH,EAAQF,GAEzB9nB,KAAKooB,UAAUN,EAAWE,GAC5B,CAGAI,UAAUpoC,EAASqoC,GACZroC,IAILA,EAAQwb,UAAUtE,IAAIkwB,IAEtBpnB,KAAKooB,UAAU9tB,GAAuBta,IAmBtCggB,KAAK2F,gBAhBY,KACsB,QAAjC3lB,EAAQga,aAAa,SAKzBha,EAAQ4B,gBAAgB,YACxB5B,EAAQ6B,aAAa,iBAAiB,GAEtCme,KAAKsoB,gBAAgBtoC,GAAS,GAE9BugB,GAAakB,QAAQzhB,EAAS4mC,GAAe,CAC3C9mB,cAAeuoB,KAVfroC,EAAQwb,UAAUtE,IAAIowB,GAWtB,GAG0BtnC,EAASA,EAAQwb,UAAUvW,SAASoiC,KACpE,CAEAc,YAAYnoC,EAASqoC,GACdroC,IAILA,EAAQwb,UAAUuH,OAAOqkB,IACzBpnC,EAAQ+6B,OAER/a,KAAKmoB,YAAY7tB,GAAuBta,IAmBxCggB,KAAK2F,gBAhBY,KACsB,QAAjC3lB,EAAQga,aAAa,SAKzBha,EAAQ6B,aAAa,iBAAiB,GACtC7B,EAAQ6B,aAAa,WAAY,MAEjCme,KAAKsoB,gBAAgBtoC,GAAS,GAE9BugB,GAAakB,QAAQzhB,EAAS0mC,GAAgB,CAC5C5mB,cAAeuoB,KAVfroC,EAAQwb,UAAUuH,OAAOukB,GAWzB,GAG0BtnC,EAASA,EAAQwb,UAAUvW,SAASoiC,KACpE,CAEAza,SAASxN,GACP,IAAK,CAAC4nB,GAAgBC,GAAiBC,GAAcC,IAAgBjtB,SAASkF,EAAM7hB,KAClF,OAGF6hB,EAAMyU,kBAENzU,EAAM+C,iBACN,MAAMoL,EAAS,CAAC0Z,GAAiBE,IAAgBjtB,SAASkF,EAAM7hB,KAC1DgrC,EAAoBzqB,GAAqBkC,KAAK6nB,eAAejhC,QAAO5G,IAAYqb,GAAWrb,KAAWof,EAAMpS,OAAQugB,GAAQ,GAE9Hgb,IACFA,EAAkB7V,MAAM,CACtB8V,eAAe,IAEjBb,GAAIthB,oBAAoBkiB,GAAmB1Y,OAE/C,CAEAgY,eAEE,OAAO5gB,GAAerU,KAAK60B,GAAqBznB,KAAKqS,QACvD,CAEA4V,iBACE,OAAOjoB,KAAK6nB,eAAej1B,MAAKzN,GAAS6a,KAAK+nB,cAAc5iC,MAAW,IACzE,CAEAyiC,sBAAsB1iC,EAAQiiB,GAC5BnH,KAAKyoB,yBAAyBvjC,EAAQ,OAAQ,WAE9C,IAAK,MAAMC,KAASgiB,EAClBnH,KAAK0oB,6BAA6BvjC,EAEtC,CAEAujC,6BAA6BvjC,GAC3BA,EAAQ6a,KAAK2oB,iBAAiBxjC,GAE9B,MAAMyjC,EAAW5oB,KAAK+nB,cAAc5iC,GAE9B0jC,EAAY7oB,KAAK8oB,iBAAiB3jC,GAExCA,EAAMtD,aAAa,gBAAiB+mC,GAEhCC,IAAc1jC,GAChB6a,KAAKyoB,yBAAyBI,EAAW,OAAQ,gBAG9CD,GACHzjC,EAAMtD,aAAa,WAAY,MAGjCme,KAAKyoB,yBAAyBtjC,EAAO,OAAQ,OAG7C6a,KAAK+oB,mCAAmC5jC,EAC1C,CAEA4jC,mCAAmC5jC,GACjC,MAAM6H,EAASsN,GAAuBnV,GAEjC6H,IAILgT,KAAKyoB,yBAAyBz7B,EAAQ,OAAQ,YAE1C7H,EAAMygC,IACR5lB,KAAKyoB,yBAAyBz7B,EAAQ,kBAAmB,IAAI7H,EAAMygC,MAEvE,CAEA0C,gBAAgBtoC,EAASgpC,GACvB,MAAMH,EAAY7oB,KAAK8oB,iBAAiB9oC,GAExC,IAAK6oC,EAAUrtB,UAAUvW,SAxMN,YAyMjB,OAGF,MAAM8hB,EAAS,CAAChN,EAAU2b,KACxB,MAAM11B,EAAUinB,GAAeC,QAAQnN,EAAU8uB,GAE7C7oC,GACFA,EAAQwb,UAAUuL,OAAO2O,EAAWsT,EACtC,EAGFjiB,EAnN6B,mBAmNIqgB,IACjCrgB,EAnN2B,iBAmNIugB,IAC/BuB,EAAUhnC,aAAa,gBAAiBmnC,EAC1C,CAEAP,yBAAyBzoC,EAASwC,EAAWpE,GACtC4B,EAAQ0b,aAAalZ,IACxBxC,EAAQ6B,aAAaW,EAAWpE,EAEpC,CAEA2pC,cAAczY,GACZ,OAAOA,EAAK9T,UAAUvW,SAASmiC,GACjC,CAGAuB,iBAAiBrZ,GACf,OAAOA,EAAKlI,QAAQqgB,IAAuBnY,EAAOrI,GAAeC,QAAQugB,GAAqBnY,EAChG,CAGAwZ,iBAAiBxZ,GACf,OAAOA,EAAKnU,QArOO,gCAqOoBmU,CACzC,CAGAzJ,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAO68B,GAAIthB,oBAAoBrG,MAErC,GAAsB,iBAAXqE,EAAX,CAIA,QAAqB7K,IAAjB1O,EAAKuZ,IAAyBA,EAAOlK,WAAW,MAAmB,gBAAXkK,EAC1D,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,IANL,CAOF,GACF,EAQF9D,GAAaY,GAAGrb,SAAU+gC,GAAsBW,IAAsB,SAAUpoB,GAC1E,CAAC,IAAK,QAAQlF,SAAS8F,KAAKoG,UAC9BhH,EAAM+C,iBAGJ9G,GAAW2E,OAIf2nB,GAAIthB,oBAAoBrG,MAAM6P,MAChC,IAKAtP,GAAaY,GAAG9gB,OAAQ0mC,IAAqB,KAC3C,IAAK,MAAM/mC,KAAWinB,GAAerU,KAAK80B,IACxCC,GAAIthB,oBAAoBrmB,EAC1B,IAMFqc,GAAmBsrB,IAYnB,MAEMniB,GAAY,YACZyjB,GAAkB,YAAYzjB,KAC9B0jB,GAAiB,WAAW1jB,KAC5B2jB,GAAgB,UAAU3jB,KAC1B4jB,GAAiB,WAAW5jB,KAC5B6jB,GAAa,OAAO7jB,KACpB8jB,GAAe,SAAS9jB,KACxB+jB,GAAa,OAAO/jB,KACpBgkB,GAAc,QAAQhkB,KAEtBikB,GAAkB,OAElBC,GAAkB,OAClBC,GAAqB,UACrBzlB,GAAc,CAClBkc,UAAW,UACXwJ,SAAU,UACVrJ,MAAO,UAEHtc,GAAU,CACdmc,WAAW,EACXwJ,UAAU,EACVrJ,MAAO,KAMT,MAAMsJ,WAAc3kB,GAClBR,YAAY1kB,EAASqkB,GACnBc,MAAMnlB,EAASqkB,GACfrE,KAAK4gB,SAAW,KAChB5gB,KAAK8pB,sBAAuB,EAC5B9pB,KAAK+pB,yBAA0B,EAE/B/pB,KAAKkhB,eACP,CAGWjd,qBACT,OAAOA,EACT,CAEWC,yBACT,OAAOA,EACT,CAEWzH,kBACT,MAlDS,OAmDX,CAGAoT,OACoBtP,GAAakB,QAAQzB,KAAKoF,SAAUmkB,IAExC1nB,mBAId7B,KAAKgqB,gBAEDhqB,KAAKqF,QAAQ+a,WACfpgB,KAAKoF,SAAS5J,UAAUtE,IArDN,QAgEpB8I,KAAKoF,SAAS5J,UAAUuH,OAAO0mB,IAG/B1tB,GAAOiE,KAAKoF,UAEZpF,KAAKoF,SAAS5J,UAAUtE,IAAIwyB,GAAiBC,IAE7C3pB,KAAK2F,gBAfY,KACf3F,KAAKoF,SAAS5J,UAAUuH,OAAO4mB,IAE/BppB,GAAakB,QAAQzB,KAAKoF,SAAUokB,IAEpCxpB,KAAKiqB,oBAAoB,GAUGjqB,KAAKoF,SAAUpF,KAAKqF,QAAQ+a,WAC5D,CAEAxQ,OACO5P,KAAKkqB,YAIQ3pB,GAAakB,QAAQzB,KAAKoF,SAAUikB,IAExCxnB,mBAad7B,KAAKoF,SAAS5J,UAAUtE,IAAIyyB,IAE5B3pB,KAAK2F,gBAXY,KACf3F,KAAKoF,SAAS5J,UAAUtE,IAAIuyB,IAG5BzpB,KAAKoF,SAAS5J,UAAUuH,OAAO4mB,GAAoBD,IAEnDnpB,GAAakB,QAAQzB,KAAKoF,SAAUkkB,GAAa,GAKrBtpB,KAAKoF,SAAUpF,KAAKqF,QAAQ+a,YAC5D,CAEA7a,UACEvF,KAAKgqB,gBAEDhqB,KAAKkqB,WACPlqB,KAAKoF,SAAS5J,UAAUuH,OAAO2mB,IAGjCvkB,MAAMI,SACR,CAEA2kB,UACE,OAAOlqB,KAAKoF,SAAS5J,UAAUvW,SAASykC,GAC1C,CAGAO,qBACOjqB,KAAKqF,QAAQukB,WAId5pB,KAAK8pB,sBAAwB9pB,KAAK+pB,0BAItC/pB,KAAK4gB,SAAW/iB,YAAW,KACzBmC,KAAK4P,MAAM,GACV5P,KAAKqF,QAAQkb,QAClB,CAEA4J,eAAe/qB,EAAOgrB,GACpB,OAAQhrB,EAAMqB,MACZ,IAAK,YACL,IAAK,WAEDT,KAAK8pB,qBAAuBM,EAC5B,MAGJ,IAAK,UACL,IAAK,WAEDpqB,KAAK+pB,wBAA0BK,EAKrC,GAAIA,EAGF,YAFApqB,KAAKgqB,gBAKP,MAAMxc,EAAcpO,EAAMU,cAEtBE,KAAKoF,WAAaoI,GAAexN,KAAKoF,SAASngB,SAASuoB,IAI5DxN,KAAKiqB,oBACP,CAEA/I,gBACE3gB,GAAaY,GAAGnB,KAAKoF,SAAU6jB,IAAiB7pB,GAASY,KAAKmqB,eAAe/qB,GAAO,KACpFmB,GAAaY,GAAGnB,KAAKoF,SAAU8jB,IAAgB9pB,GAASY,KAAKmqB,eAAe/qB,GAAO,KACnFmB,GAAaY,GAAGnB,KAAKoF,SAAU+jB,IAAe/pB,GAASY,KAAKmqB,eAAe/qB,GAAO,KAClFmB,GAAaY,GAAGnB,KAAKoF,SAAUgkB,IAAgBhqB,GAASY,KAAKmqB,eAAe/qB,GAAO,IACrF,CAEA4qB,gBACE/c,aAAajN,KAAK4gB,UAClB5gB,KAAK4gB,SAAW,IAClB,CAGA/a,uBAAuBxB,GACrB,OAAOrE,KAAK4G,MAAK,WACf,MAAM9b,EAAO++B,GAAMxjB,oBAAoBrG,KAAMqE,GAE7C,GAAsB,iBAAXA,EAAqB,CAC9B,QAA4B,IAAjBvZ,EAAKuZ,GACd,MAAM,IAAIW,UAAU,oBAAoBX,MAG1CvZ,EAAKuZ,GAAQrE,KACf,CACF,GACF,ECxjKK,SAASqqB,GAAc9tB,GACD,WAAvBzW,SAASgX,WAAyBP,IACjCzW,SAASyF,iBAAiB,mBAAoBgR,EACrD,CD6jKAyJ,GAAqB6jB,IAKrBxtB,GAAmBwtB,IEzhKnBQ,IAvCA,WAC2B,GAAGx3B,MAAM5U,KAChC6H,SAASsa,iBAAiB,+BAET7c,KAAI,SAAU+mC,GAC/B,OAAO,IAAI5J,GAAQ4J,EAAkB,CAAE/J,MAAO,CAAE1Q,KAAM,IAAKD,KAAM,MACnE,GACF,IAiCAya,IA5BA,WACYvkC,SAAS08B,eAAe,mBAC9Bj3B,iBAAiB,SAAS,WAC5BzF,SAAS6G,KAAKT,UAAY,EAC1BpG,SAASC,gBAAgBmG,UAAY,CACvC,GACF,IAuBAm+B,IArBA,WACE,IAAIE,EAAMzkC,SAAS08B,eAAe,mBAC9BgI,EAAS1kC,SACV2kC,uBAAuB,aAAa,GACpC1mC,wBACH1D,OAAOkL,iBAAiB,UAAU,WAC5ByU,KAAK0qB,UAAY1qB,KAAK2qB,SAAW3qB,KAAK2qB,QAAUH,EAAOnsC,OACzDksC,EAAI/oC,MAAMwwB,QAAU,QAEpBuY,EAAI/oC,MAAMwwB,QAAU,OAEtBhS,KAAK0qB,UAAY1qB,KAAK2qB,OACxB,GACF","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (process.env.NODE_ENV !== \"production\") {\n if (!isHTMLElement(arrowElement)) {\n console.error(['Popper: \"arrow\" element must be an HTMLElement (not an SVGElement).', 'To use an SVG arrow, wrap it in an HTMLElement that will be used as', 'the arrow.'].join(' '));\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(['Popper: \"arrow\" modifier\\'s `element` must be a child of the popper', 'element.'].join(' '));\n }\n\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n\n if (process.env.NODE_ENV !== \"production\") {\n var transitionProperty = getComputedStyle(state.elements.popper).transitionProperty || '';\n\n if (adaptive && ['transform', 'top', 'right', 'bottom', 'left'].some(function (property) {\n return transitionProperty.indexOf(property) >= 0;\n })) {\n console.warn(['Popper: Detected CSS transitions on at least one of the following', 'CSS properties: \"transform\", \"top\", \"right\", \"bottom\", \"left\".', '\\n\\n', 'Disable the \"computeStyles\" modifier\\'s `adaptive` option to allow', 'for smooth transitions, or remove these properties from the CSS', 'transition declaration on the popper element if only transitioning', 'opacity or background-color for example.', '\\n\\n', 'We recommend using the popper element as a wrapper around an inner', 'element that can have any CSS property transitioned for animations.'].join(' '));\n }\n }\n\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n\n if (process.env.NODE_ENV !== \"production\") {\n console.error(['Popper: The `allowedAutoPlacements` option did not allow any', 'placements. Ensure the `placement` option matches the variation', 'of the allowed placements.', 'For example, \"auto\" cannot be used to allow \"bottom-start\".', 'Use \"auto-start\" instead.'].join(' '));\n }\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport getComputedStyle from \"./dom-utils/getComputedStyle.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport validateModifiers from \"./utils/validateModifiers.js\";\nimport uniqueBy from \"./utils/uniqueBy.js\";\nimport getBasePlacement from \"./utils/getBasePlacement.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nimport { auto } from \"./enums.js\";\nvar INVALID_ELEMENT_ERROR = 'Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.';\nvar INFINITE_LOOP_ERROR = 'Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash.';\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n }); // Validate the provided modifiers so that the consumer will get warned\n // if one of the modifiers is invalid for any reason\n\n if (process.env.NODE_ENV !== \"production\") {\n var modifiers = uniqueBy([].concat(orderedModifiers, state.options.modifiers), function (_ref) {\n var name = _ref.name;\n return name;\n });\n validateModifiers(modifiers);\n\n if (getBasePlacement(state.options.placement) === auto) {\n var flipModifier = state.orderedModifiers.find(function (_ref2) {\n var name = _ref2.name;\n return name === 'flip';\n });\n\n if (!flipModifier) {\n console.error(['Popper: \"auto\" placements require the \"flip\" modifier be', 'present and enabled to work.'].join(' '));\n }\n }\n\n var _getComputedStyle = getComputedStyle(popper),\n marginTop = _getComputedStyle.marginTop,\n marginRight = _getComputedStyle.marginRight,\n marginBottom = _getComputedStyle.marginBottom,\n marginLeft = _getComputedStyle.marginLeft; // We no longer take into account `margins` on the popper, and it can\n // cause bugs with positioning, so we'll warn the consumer\n\n\n if ([marginTop, marginRight, marginBottom, marginLeft].some(function (margin) {\n return parseFloat(margin);\n })) {\n console.warn(['Popper: CSS \"margin\" styles cannot be used to apply padding', 'between the popper and its reference element or boundary.', 'To replicate margin, use the `offset` modifier, as well as', 'the `padding` option in the `preventOverflow` and `flip`', 'modifiers.'].join(' '));\n }\n }\n\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(INVALID_ELEMENT_ERROR);\n }\n\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n var __debug_loops__ = 0;\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (process.env.NODE_ENV !== \"production\") {\n __debug_loops__ += 1;\n\n if (__debug_loops__ > 100) {\n console.error(INFINITE_LOOP_ERROR);\n break;\n }\n }\n\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n if (process.env.NODE_ENV !== \"production\") {\n console.error(INVALID_ELEMENT_ERROR);\n }\n\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref3) {\n var name = _ref3.name,\n _ref3$options = _ref3.options,\n options = _ref3$options === void 0 ? {} : _ref3$options,\n effect = _ref3.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend'; // Shout-out Angus Croll (https://goo.gl/pxwQGp)\n\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n/**\n * Public Util API\n */\n\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n\n return prefix;\n};\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href'); // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n } // Just in case some CMS puts out a full URL with the anchor appended\n\n\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n\n return selector;\n};\n\nconst getSelectorFromElement = element => {\n const selector = getSelector(element);\n\n if (selector) {\n return document.querySelector(selector) ? selector : null;\n }\n\n return null;\n};\n\nconst getElementFromSelector = element => {\n const selector = getSelector(element);\n return selector ? document.querySelector(selector) : null;\n};\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n } // Get transition-duration of the element\n\n\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay); // Return 0 if element or transition duration is not found\n\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n } // If multiple durations are defined, take the first\n\n\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\n\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n\n return typeof object.nodeType !== 'undefined';\n};\n\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(object);\n }\n\n return null;\n};\n\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; // Handle `details` element as its content may falsie appear visible when it is closed\n\n const closedDetails = element.closest('details:not([open])');\n\n if (!closedDetails) {\n return elementIsVisible;\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n\n if (summary === null) {\n return false;\n }\n }\n\n return elementIsVisible;\n};\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n\n if (element.classList.contains('disabled')) {\n return true;\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n } // Can find the shadow root otherwise it'll return the document\n\n\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n\n if (element instanceof ShadowRoot) {\n return element;\n } // when we don't find a shadow root\n\n\n if (!element.parentNode) {\n return null;\n }\n\n return findShadowRoot(element.parentNode);\n};\n\nconst noop = () => {};\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\n\n\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n\n return null;\n};\n\nconst DOMContentLoadedCallbacks = [];\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\n\nconst isRTL = () => document.documentElement.dir === 'rtl';\n\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\n\nconst execute = callback => {\n if (typeof callback === 'function') {\n callback();\n }\n};\n\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\n\n\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement); // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n\n index += shouldGetNext ? 1 : -1;\n\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\n\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\n\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n\n return fn.apply(element, [event]);\n };\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n\n hydrateObj(event, {\n delegateTarget: target\n });\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n\n return fn.apply(target, [event]);\n }\n }\n };\n}\n\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\n\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string'; // todo: tooltip passes `false` instead of selector, so we need to check\n\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n\n return [isDelegated, callable, typeEvent];\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n\n callable = wrapFunction(callable);\n }\n\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n\n if (!fn) {\n return;\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n\n for (const handlerKey of Object.keys(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n const event = storeElementEvent[handlerKey];\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\n\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n\n for (const keyHandlers of Object.keys(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n const event = storeElementEvent[keyHandlers];\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n\n let evt = new Event(event, {\n bubbles,\n cancelable: true\n });\n evt = hydrateObj(evt, args);\n\n if (defaultPrevented) {\n evt.preventDefault();\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n\n return evt;\n }\n\n};\n\nfunction hydrateObj(obj, meta) {\n for (const [key, value] of Object.entries(meta || {})) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n\n get() {\n return value;\n }\n\n });\n }\n }\n\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n\n const instanceMap = elementMap.get(element); // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n\n instanceMap.set(key, instance);\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n\n return null;\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key); // free up element references if there are no instances left for an element\n\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n\n if (value === 'false') {\n return false;\n }\n\n if (value === Number(value).toString()) {\n return Number(value);\n }\n\n if (value === '' || value === 'null') {\n return null;\n }\n\n if (typeof value !== 'string') {\n return value;\n }\n\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n\n return attributes;\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n\n static get DefaultType() {\n return {};\n }\n\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n\n this._typeCheckConfig(config);\n\n return config;\n }\n\n _configAfterMerge(config) {\n return config;\n }\n\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return { ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const property of Object.keys(configTypes)) {\n const expectedTypes = configTypes[property];\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst VERSION = '5.2.3';\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n\n if (!element) {\n return;\n }\n\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n } // Public\n\n\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n\n this._typeCheckConfig(config);\n\n return config;\n } // Static\n\n\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n\n static get VERSION() {\n return VERSION;\n }\n\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n\n if (isDisabled(this)) {\n return;\n }\n\n const target = getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target); // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n } // Public\n\n\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n\n if (closeEvent.defaultPrevented) {\n return;\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n } // Private\n\n\n _destroyElement() {\n this._element.remove();\n\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config](this);\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nenableDismissTrigger(Alert, 'close');\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n } // Public\n\n\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n\n return parents;\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling;\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n\n previous = previous.previousElementSibling;\n }\n\n return [];\n },\n\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n\n next = next.nextElementSibling;\n }\n\n return [];\n },\n\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n }\n\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n\n if (!element || !Swipe.isSupported()) {\n return;\n }\n\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n\n this._initEvents();\n } // Getters\n\n\n static get Default() {\n return Default$c;\n }\n\n static get DefaultType() {\n return DefaultType$c;\n }\n\n static get NAME() {\n return NAME$d;\n } // Public\n\n\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n } // Private\n\n\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n\n this._handleSwipe();\n\n execute(this._config.endCallback);\n }\n\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n\n if (!direction) {\n return;\n }\n\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n } // Static\n\n\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n\n this._addEventListeners();\n\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n } // Getters\n\n\n static get Default() {\n return Default$b;\n }\n\n static get DefaultType() {\n return DefaultType$b;\n }\n\n static get NAME() {\n return NAME$c;\n } // Public\n\n\n next() {\n this._slide(ORDER_NEXT);\n }\n\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n\n prev() {\n this._slide(ORDER_PREV);\n }\n\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n\n this._clearInterval();\n }\n\n cycle() {\n this._clearInterval();\n\n this._updateInterval();\n\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n\n this.cycle();\n }\n\n to(index) {\n const items = this._getItems();\n\n if (index > items.length - 1 || index < 0) {\n return;\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n\n const activeIndex = this._getItemIndex(this._getActive());\n\n if (activeIndex === index) {\n return;\n }\n\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n\n this._slide(order, items[index]);\n }\n\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n\n super.dispose();\n } // Private\n\n\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n } // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n\n this.pause();\n\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n\n const direction = KEY_TO_DIRECTION[event.key];\n\n if (direction) {\n event.preventDefault();\n\n this._slide(this._directionToOrder(direction));\n }\n }\n\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n\n if (!element) {\n return;\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n\n const activeElement = this._getActive();\n\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n\n if (nextElement === activeElement) {\n return;\n }\n\n const nextElementIndex = this._getItemIndex(nextElement);\n\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n\n const slideEvent = triggerEvent(EVENT_SLIDE);\n\n if (slideEvent.defaultPrevented) {\n return;\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // todo: change tests that use empty divs to avoid this check\n return;\n }\n\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n\n this._setActiveIndicatorElement(nextElementIndex);\n\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n\n if (isCycling) {\n this.cycle();\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config]();\n }\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = getElementFromSelector(this);\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n\n if (slideIndex) {\n carousel.to(slideIndex);\n\n carousel._maybeEnableCycle();\n\n return;\n }\n\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n\n carousel._maybeEnableCycle();\n\n return;\n }\n\n carousel.prev();\n\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n\n for (const elem of toggleList) {\n const selector = getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n\n this._initializeChildren();\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n\n if (this._config.toggle) {\n this.toggle();\n }\n } // Getters\n\n\n static get Default() {\n return Default$a;\n }\n\n static get DefaultType() {\n return DefaultType$a;\n }\n\n static get NAME() {\n return NAME$b;\n } // Public\n\n\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n\n let activeChildren = []; // find active children\n\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n\n if (startEvent.defaultPrevented) {\n return;\n }\n\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n\n const dimension = this._getDimension();\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n\n this._element.style[dimension] = 0;\n\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n\n this._isTransitioning = true;\n\n const complete = () => {\n this._isTransitioning = false;\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n\n this._queueCallback(complete, this._element, true);\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n\n if (startEvent.defaultPrevented) {\n return;\n }\n\n const dimension = this._getDimension();\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n\n for (const trigger of this._triggerArray) {\n const element = getElementFromSelector(trigger);\n\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n\n this._isTransitioning = true;\n\n const complete = () => {\n this._isTransitioning = false;\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n\n this._element.style[dimension] = '';\n\n this._queueCallback(complete, this._element, true);\n }\n\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n } // Private\n\n\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n\n config.parent = getElement(config.parent);\n return config;\n }\n\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n\n for (const element of children) {\n const selected = getElementFromSelector(element);\n\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); // remove children if greater depth\n\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n } // Static\n\n\n static jQueryInterface(config) {\n const _config = {};\n\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config]();\n }\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n\n const selector = getSelectorFromElement(this);\n const selectorElements = SelectorEngine.find(selector);\n\n for (const element of selectorElements) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // todo: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.2/forms/input-group/\n\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n } // Getters\n\n\n static get Default() {\n return Default$9;\n }\n\n static get DefaultType() {\n return DefaultType$9;\n }\n\n static get NAME() {\n return NAME$a;\n } // Public\n\n\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n\n if (showEvent.defaultPrevented) {\n return;\n }\n\n this._createPopper(); // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n\n this._element.focus();\n\n this._element.setAttribute('aria-expanded', true);\n\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n\n this._element.classList.add(CLASS_NAME_SHOW$6);\n\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n };\n\n this._completeHide(relatedTarget);\n }\n\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n\n super.dispose();\n }\n\n update() {\n this._inNavbar = this._detectNavbar();\n\n if (this._popper) {\n this._popper.update();\n }\n } // Private\n\n\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n\n if (hideEvent.defaultPrevented) {\n return;\n } // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n\n\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n\n if (this._popper) {\n this._popper.destroy();\n }\n\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n\n this._element.setAttribute('aria-expanded', 'false');\n\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n\n _getConfig(config) {\n config = super._getConfig(config);\n\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n\n return config;\n }\n\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n\n let referenceElement = this._element;\n\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n\n const popperConfig = this._getPopperConfig();\n\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n\n _getPlacement() {\n const parentDropdown = this._parent;\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n } // We need to trim the value because custom properties can also include spaces\n\n\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n\n _getOffset() {\n const {\n offset\n } = this._config;\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n\n return offset;\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }; // Disable Popper if we have a static display or Dropdown is in Navbar\n\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // todo:v6 remove\n\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n\n return { ...defaultBsPopperConfig,\n ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)\n };\n }\n\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n\n if (!items.length) {\n return;\n } // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n\n\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config]();\n });\n }\n\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n\n if (!context || context._config.autoClose === false) {\n continue;\n }\n\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n } // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n\n\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n\n const relatedTarget = {\n relatedTarget: context._element\n };\n\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n\n context._completeHide(relatedTarget);\n }\n }\n\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n\n if (isInput && !isEscapeEvent) {\n return;\n }\n\n event.preventDefault(); // todo: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.2/forms/input-group/\n\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n\n instance._selectMenuItem(event);\n\n return;\n }\n\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n } // Public\n\n\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n\n hide() {\n const width = this.getWidth();\n\n this._disableOverFlow(); // give padding to element to balance the hidden scrollbar width\n\n\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n\n\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n\n isOverflowing() {\n return this.getWidth() > 0;\n } // Private\n\n\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n\n this._element.style.overflow = 'hidden';\n }\n\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n\n this._saveInitialAttribute(element, styleProperty);\n\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty); // We only want to remove the property if the value is `null`; the value can also be zero\n\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n } // Getters\n\n\n static get Default() {\n return Default$8;\n }\n\n static get DefaultType() {\n return DefaultType$8;\n }\n\n static get NAME() {\n return NAME$9;\n } // Public\n\n\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n\n this._append();\n\n const element = this._getElement();\n\n if (this._config.isAnimated) {\n reflow(element);\n }\n\n element.classList.add(CLASS_NAME_SHOW$5);\n\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n\n dispose() {\n if (!this._isAppended) {\n return;\n }\n\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n\n this._element.remove();\n\n this._isAppended = false;\n } // Private\n\n\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n\n this._element = backdrop;\n }\n\n return this._element;\n }\n\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n\n _append() {\n if (this._isAppended) {\n return;\n }\n\n const element = this._getElement();\n\n this._config.rootElement.append(element);\n\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n } // Getters\n\n\n static get Default() {\n return Default$7;\n }\n\n static get DefaultType() {\n return DefaultType$7;\n }\n\n static get NAME() {\n return NAME$8;\n } // Public\n\n\n activate() {\n if (this._isActive) {\n return;\n }\n\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n\n deactivate() {\n if (!this._isActive) {\n return;\n }\n\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n } // Private\n\n\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n\n const elements = SelectorEngine.focusableChildren(trapElement);\n\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n\n this._addEventListeners();\n } // Getters\n\n\n static get Default() {\n return Default$6;\n }\n\n static get DefaultType() {\n return DefaultType$6;\n }\n\n static get NAME() {\n return NAME$7;\n } // Public\n\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n\n if (showEvent.defaultPrevented) {\n return;\n }\n\n this._isShown = true;\n this._isTransitioning = true;\n\n this._scrollBar.hide();\n\n document.body.classList.add(CLASS_NAME_OPEN);\n\n this._adjustDialog();\n\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n this._isShown = false;\n this._isTransitioning = true;\n\n this._focustrap.deactivate();\n\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n\n dispose() {\n for (const htmlElement of [window, this._dialog]) {\n EventHandler.off(htmlElement, EVENT_KEY$4);\n }\n\n this._backdrop.dispose();\n\n this._focustrap.deactivate();\n\n super.dispose();\n }\n\n handleUpdate() {\n this._adjustDialog();\n } // Private\n\n\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n\n this._element.style.display = 'block';\n\n this._element.removeAttribute('aria-hidden');\n\n this._element.setAttribute('aria-modal', true);\n\n this._element.setAttribute('role', 'dialog');\n\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n\n reflow(this._element);\n\n this._element.classList.add(CLASS_NAME_SHOW$4);\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n\n if (this._config.keyboard) {\n event.preventDefault();\n this.hide();\n return;\n }\n\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n\n return;\n }\n\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n\n _hideModal() {\n this._element.style.display = 'none';\n\n this._element.setAttribute('aria-hidden', true);\n\n this._element.removeAttribute('aria-modal');\n\n this._element.removeAttribute('role');\n\n this._isTransitioning = false;\n\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n\n this._resetAdjustments();\n\n this._scrollBar.reset();\n\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY; // return if the following background transition hasn't yet completed\n\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n\n this._element.classList.add(CLASS_NAME_STATIC);\n\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n\n this._element.focus();\n }\n /**\n * The following methods are used to handle overflowing modals\n */\n\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n\n const scrollbarWidth = this._scrollBar.getWidth();\n\n const isBodyOverflowing = scrollbarWidth > 0;\n\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n } // Static\n\n\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config](relatedTarget);\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = getElementFromSelector(this);\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n }); // avoid conflict when clicking modal toggler while another one is open\n\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n\n this._addEventListeners();\n } // Getters\n\n\n static get Default() {\n return Default$5;\n }\n\n static get DefaultType() {\n return DefaultType$5;\n }\n\n static get NAME() {\n return NAME$6;\n } // Public\n\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n\n if (showEvent.defaultPrevented) {\n return;\n }\n\n this._isShown = true;\n\n this._backdrop.show();\n\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n\n this._element.setAttribute('aria-modal', true);\n\n this._element.setAttribute('role', 'dialog');\n\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n\n this._element.classList.add(CLASS_NAME_SHOW$3);\n\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n\n this._queueCallback(completeCallBack, this._element, true);\n }\n\n hide() {\n if (!this._isShown) {\n return;\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n this._focustrap.deactivate();\n\n this._element.blur();\n\n this._isShown = false;\n\n this._element.classList.add(CLASS_NAME_HIDING);\n\n this._backdrop.hide();\n\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n\n this._element.removeAttribute('aria-modal');\n\n this._element.removeAttribute('role');\n\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n\n this._queueCallback(completeCallback, this._element, true);\n }\n\n dispose() {\n this._backdrop.dispose();\n\n this._focustrap.deactivate();\n\n super.dispose();\n } // Private\n\n\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n\n this.hide();\n }; // 'static' option will be translated to true, and booleans will keep their value\n\n\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n\n if (!this._config.keyboard) {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n\n this.hide();\n });\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config](this);\n });\n }\n\n}\n/**\n * Data API implementation\n */\n\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = getElementFromSelector(this);\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n\n if (isDisabled(this)) {\n return;\n }\n\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n }); // avoid conflict when clicking a toggler of an offcanvas, while another is open\n\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n/**\n * A pattern that recognizes a commonly useful subset of URLs that are safe.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts\n */\n\nconst SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i;\n/**\n * A pattern that matches safe data URLs. Only matches image, video and audio types.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts\n */\n\nconst DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[\\d+/a-z]+=*$/i;\n\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue));\n }\n\n return true;\n } // Check if a regular expression validates the attribute.\n\n\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\n\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n } // Getters\n\n\n static get Default() {\n return Default$4;\n }\n\n static get DefaultType() {\n return DefaultType$4;\n }\n\n static get NAME() {\n return NAME$5;\n } // Public\n\n\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n\n hasContent() {\n return this.getContent().length > 0;\n }\n\n changeContent(content) {\n this._checkContent(content);\n\n this._config.content = { ...this._config.content,\n ...content\n };\n return this;\n }\n\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n\n const template = templateWrapper.children[0];\n\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n\n return template;\n } // Private\n\n\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n\n this._checkContent(config.content);\n }\n\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n\n if (!templateElement) {\n return;\n }\n\n content = this._resolvePossibleFunction(content);\n\n if (!content) {\n templateElement.remove();\n return;\n }\n\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n\n return;\n }\n\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n\n templateElement.textContent = content;\n }\n\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n\n _resolvePossibleFunction(arg) {\n return typeof arg === 'function' ? arg(this) : arg;\n }\n\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n\n templateElement.textContent = element.textContent;\n }\n\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 0],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n\n super(element, config); // Private\n\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null; // Protected\n\n this.tip = null;\n\n this._setListeners();\n\n if (!this._config.selector) {\n this._fixTitle();\n }\n } // Getters\n\n\n static get Default() {\n return Default$3;\n }\n\n static get DefaultType() {\n return DefaultType$3;\n }\n\n static get NAME() {\n return NAME$4;\n } // Public\n\n\n enable() {\n this._isEnabled = true;\n }\n\n disable() {\n this._isEnabled = false;\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n\n this._activeTrigger.click = !this._activeTrigger.click;\n\n if (this._isShown()) {\n this._leave();\n\n return;\n }\n\n this._enter();\n }\n\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n\n this._disposePopper();\n\n super.dispose();\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n } // todo v6 remove this OR make it optional\n\n\n this._disposePopper();\n\n const tip = this._getTipElement();\n\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n\n const {\n container\n } = this._config;\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2); // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n\n if (this._isHovered === false) {\n this._leave();\n }\n\n this._isHovered = false;\n };\n\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n\n hide() {\n if (!this._isShown()) {\n return;\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n const tip = this._getTipElement();\n\n tip.classList.remove(CLASS_NAME_SHOW$2); // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n\n if (!this._isHovered) {\n this._disposePopper();\n }\n\n this._element.removeAttribute('aria-describedby');\n\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n\n update() {\n if (this._popper) {\n this._popper.update();\n }\n } // Protected\n\n\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n\n return this.tip;\n }\n\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml(); // todo: remove this check on v6\n\n\n if (!tip) {\n return null;\n }\n\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); // todo: on v6 the following can be achieved with CSS only\n\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n\n return tip;\n }\n\n setContent(content) {\n this._newContent = content;\n\n if (this._isShown()) {\n this._disposePopper();\n\n this.show();\n }\n }\n\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({ ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n\n return this._templateFactory;\n }\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n } // Private\n\n\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n\n _createPopper(tip) {\n const placement = typeof this._config.placement === 'function' ? this._config.placement.call(this, tip, this._element) : this._config.placement;\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n\n _getOffset() {\n const {\n offset\n } = this._config;\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n\n return offset;\n }\n\n _resolvePossibleFunction(arg) {\n return typeof arg === 'function' ? arg.call(this._element) : arg;\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return { ...defaultBsPopperConfig,\n ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)\n };\n }\n\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n\n context._leave();\n });\n }\n }\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title');\n\n if (!title) {\n return;\n }\n\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n\n\n this._element.removeAttribute('title');\n }\n\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n\n this._isHovered = true;\n\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n\n this._isHovered = false;\n\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n\n config = { ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n\n this._typeCheckConfig(config);\n\n return config;\n }\n\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n\n return config;\n }\n\n _getDelegateConfig() {\n const config = {};\n\n for (const key in this._config) {\n if (this.constructor.Default[key] !== this._config[key]) {\n config[key] = this._config[key];\n }\n }\n\n config.selector = false;\n config.trigger = 'manual'; // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n\n return config;\n }\n\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n\n this._popper = null;\n }\n\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config]();\n });\n }\n\n}\n/**\n * jQuery\n */\n\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = { ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = { ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n\n static get DefaultType() {\n return DefaultType$2;\n }\n\n static get NAME() {\n return NAME$3;\n } // Overrides\n\n\n _isWithContent() {\n return this._getTitle() || this._getContent();\n } // Private\n\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n } // Static\n\n\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n\n if (typeof config !== 'string') {\n return;\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n\n data[config]();\n });\n }\n\n}\n/**\n * jQuery\n */\n\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v5.2.3): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config); // this._element is the observablesContainer and config.target the menu links wrapper\n\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n } // Getters\n\n\n static get Default() {\n return Default$1;\n }\n\n static get DefaultType() {\n return DefaultType$1;\n }\n\n static get NAME() {\n return NAME$2;\n } // Public\n\n\n refresh() {\n this._initializeTargetsAndObservables();\n\n this._maybeEnableSmoothScroll();\n\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n\n dispose() {\n this._observer.disconnect();\n\n super.dispose();\n } // Private\n\n\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body; // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n\n return config;\n }\n\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n } // unregister any previous listeners\n\n\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n } // Chrome 60 doesn't support `scrollTo`\n\n\n root.scrollTop = height;\n }\n });\n }\n\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n } // The logic of selection\n\n\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n\n this._process(targetElement(entry));\n };\n\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n\n this._clearActiveClass(targetElement(entry));\n\n continue;\n }\n\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; // if we are scrolling down, pick the bigger offsetTop\n\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry); // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n\n if (!parentScrollTop) {\n return;\n }\n\n continue;\n } // if we are scrolling up, pick the smallest offsetTop\n\n\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n\n const observableSection = SelectorEngine.findOne(anchor.hash, this._element); // ensure that the observableSection exists & is visible\n\n if (isVisible(observableSection)) {\n this._targetLinks.set(anchor.hash, anchor);\n\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n\n this._clearActiveClass(this._config.target);\n\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n\n this._activateParents(target);\n\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both
    and